// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "./AereSpokePool.sol"; /** * @title AereERC7683 * @notice ERC-7683 IOriginSettler implementation for AERE. * Lets standards-compliant intent aggregators (UniswapX, CoW, Across UI, * 1inch Fusion) submit cross-chain intents to AERE via a single uniform * interface. The settler delegates execution to AereSpokePool. * * Reference: https://eips.ethereum.org/EIPS/eip-7683 * * Two intent flavours: * - GaslessCrossChainOrder: user signs an off-chain intent, anyone can open it. * The user pays gas indirectly via the filler. Used for true gasless UX. * - OnchainCrossChainOrder: user submits the intent on-chain themselves. * Used by aggregators that prefer simpler flow. * * We implement both. Both delegate to the underlying AereSpokePool. */ struct OnchainCrossChainOrder { uint32 fillDeadline; bytes32 orderDataType; bytes orderData; } struct GaslessCrossChainOrder { address originSettler; address user; uint256 nonce; uint256 originChainId; uint32 openDeadline; uint32 fillDeadline; bytes32 orderDataType; bytes orderData; } struct Output { bytes32 token; uint256 amount; bytes32 recipient; uint256 chainId; } struct FillInstruction { uint64 destinationChainId; bytes32 destinationSettler; bytes originData; } struct ResolvedCrossChainOrder { address user; uint256 originChainId; uint32 openDeadline; uint32 fillDeadline; bytes32 orderId; Output[] maxSpent; Output[] minReceived; FillInstruction[] fillInstructions; } interface IOriginSettler { event Open(bytes32 indexed orderId, ResolvedCrossChainOrder resolvedOrder); function open(OnchainCrossChainOrder calldata order) external; function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) external; function resolve(OnchainCrossChainOrder calldata order) external view returns (ResolvedCrossChainOrder memory); function resolveFor(GaslessCrossChainOrder calldata order, bytes calldata originFillerData) external view returns (ResolvedCrossChainOrder memory); } contract AereERC7683 is IOriginSettler, EIP712, ReentrancyGuard { using SafeERC20 for IERC20; /// AERE-side SpokePool we route deposits through. AereSpokePool public immutable spokePool; /// Order-data type hash that this settler accepts. /// Off-chain code constructs orderData as abi.encode(AereOrder) and uses this constant. bytes32 public constant AERE_ORDER_TYPE = keccak256("AereCrossChainOrder(address inputToken,uint256 inputAmount,address outputToken,uint256 outputAmount,uint32 destinationChainId,address recipient)"); /// EIP-712 type hash for gasless orders. bytes32 private constant GASLESS_ORDER_TYPEHASH = keccak256( "GaslessCrossChainOrder(address originSettler,address user,uint256 nonce,uint256 originChainId,uint32 openDeadline,uint32 fillDeadline,bytes32 orderDataType,bytes orderData)" ); /// Per-user nonce tracking for gasless orders. mapping(address => mapping(uint256 => bool)) public usedNonces; struct AereOrderInner { address inputToken; uint256 inputAmount; address outputToken; uint256 outputAmount; uint32 destinationChainId; address recipient; } constructor(AereSpokePool _spokePool) EIP712("AereERC7683", "1") { spokePool = _spokePool; } // ───────────────────── Onchain order ───────────────────── function open(OnchainCrossChainOrder calldata order) external override nonReentrant { require(order.orderDataType == AERE_ORDER_TYPE, "ERC7683: unknown orderDataType"); require(order.fillDeadline > block.timestamp, "ERC7683: deadline passed"); AereOrderInner memory inner = abi.decode(order.orderData, (AereOrderInner)); // Pull the input tokens from msg.sender first, then approve SpokePool to take them. IERC20(inner.inputToken).safeTransferFrom(msg.sender, address(this), inner.inputAmount); IERC20(inner.inputToken).forceApprove(address(spokePool), inner.inputAmount); spokePool.deposit( inner.inputToken, inner.inputAmount, inner.outputToken, inner.outputAmount, inner.destinationChainId, inner.recipient, order.fillDeadline ); bytes32 orderId = keccak256(abi.encode(msg.sender, order)); emit Open(orderId, _resolve(msg.sender, order.fillDeadline, 0, inner, orderId)); } function resolve(OnchainCrossChainOrder calldata order) external view override returns (ResolvedCrossChainOrder memory) { require(order.orderDataType == AERE_ORDER_TYPE, "ERC7683: unknown orderDataType"); AereOrderInner memory inner = abi.decode(order.orderData, (AereOrderInner)); bytes32 orderId = keccak256(abi.encode(msg.sender, order)); return _resolve(msg.sender, order.fillDeadline, 0, inner, orderId); } // ───────────────────── Gasless order ───────────────────── function openFor( GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata /*originFillerData*/ ) external override nonReentrant { require(order.originSettler == address(this), "ERC7683: wrong settler"); require(order.originChainId == block.chainid, "ERC7683: wrong chain"); require(order.orderDataType == AERE_ORDER_TYPE, "ERC7683: unknown orderDataType"); require(order.openDeadline > block.timestamp, "ERC7683: open deadline passed"); require(order.fillDeadline > block.timestamp, "ERC7683: fill deadline passed"); require(!usedNonces[order.user][order.nonce], "ERC7683: nonce used"); // Verify user signature bytes32 structHash = keccak256(abi.encode( GASLESS_ORDER_TYPEHASH, order.originSettler, order.user, order.nonce, order.originChainId, order.openDeadline, order.fillDeadline, order.orderDataType, keccak256(order.orderData) )); bytes32 digest = _hashTypedDataV4(structHash); address signer = ECDSA.recover(digest, signature); require(signer == order.user, "ERC7683: bad signature"); usedNonces[order.user][order.nonce] = true; AereOrderInner memory inner = abi.decode(order.orderData, (AereOrderInner)); // Pull input from the USER (they pre-approved this settler). IERC20(inner.inputToken).safeTransferFrom(order.user, address(this), inner.inputAmount); IERC20(inner.inputToken).forceApprove(address(spokePool), inner.inputAmount); spokePool.deposit( inner.inputToken, inner.inputAmount, inner.outputToken, inner.outputAmount, inner.destinationChainId, inner.recipient, order.fillDeadline ); bytes32 orderId = keccak256(abi.encode(order)); emit Open(orderId, _resolve(order.user, order.fillDeadline, order.openDeadline, inner, orderId)); } function resolveFor( GaslessCrossChainOrder calldata order, bytes calldata /*originFillerData*/ ) external view override returns (ResolvedCrossChainOrder memory) { require(order.orderDataType == AERE_ORDER_TYPE, "ERC7683: unknown orderDataType"); AereOrderInner memory inner = abi.decode(order.orderData, (AereOrderInner)); bytes32 orderId = keccak256(abi.encode(order)); return _resolve(order.user, order.fillDeadline, order.openDeadline, inner, orderId); } // ───────────────────── Internal ───────────────────── function _resolve( address user, uint32 fillDeadline, uint32 openDeadline, AereOrderInner memory inner, bytes32 orderId ) internal view returns (ResolvedCrossChainOrder memory r) { r.user = user; r.originChainId = block.chainid; r.openDeadline = openDeadline; r.fillDeadline = fillDeadline; r.orderId = orderId; r.maxSpent = new Output[](1); r.maxSpent[0] = Output({ token: bytes32(uint256(uint160(inner.inputToken))), amount: inner.inputAmount, recipient: bytes32(uint256(uint160(address(spokePool)))), chainId: block.chainid }); r.minReceived = new Output[](1); r.minReceived[0] = Output({ token: bytes32(uint256(uint160(inner.outputToken))), amount: inner.outputAmount, recipient: bytes32(uint256(uint160(inner.recipient))), chainId: inner.destinationChainId }); r.fillInstructions = new FillInstruction[](1); r.fillInstructions[0] = FillInstruction({ destinationChainId: uint64(inner.destinationChainId), destinationSettler: spokePool.remoteSpokePool(inner.destinationChainId), originData: abi.encode( block.chainid, inner, fillDeadline, user ) }); } }