// 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/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "./AereVaultRelayer.sol"; import "./AereSolverRegistry.sol"; /** * @title AereSettlement * @notice Batch-auction MEV-resistant DEX for AERE. * * Users sign EIP-712 orders off-chain (no gas). An authorised solver collects * open orders, finds a uniform clearing price per token pair, and submits the * whole batch atomically via `settle()`. The contract: * 1. Verifies each user's EIP-712 signature. * 2. Checks each order's deadline and per-token cap. * 3. Pulls each order's sellAmount via AereVaultRelayer (users approved that contract). * 4. Routes the inter-order trades atomically — coincidence-of-wants matches * at the uniform price. * 5. Pays each user their executedBuyAmount (which must be ≥ minBuyAmount). * * MEV protection comes from atomicity: the whole batch executes in one transaction, * so no actor can insert a sandwich tx between two orders in the batch. * * Permissioned solvers in Phase 1 (AereSolverRegistry-controlled). Phase 2 adds * permissionless bonded-solver entry. * * This is a simplified port of CoW Protocol's GPv2Settlement — captures the core * batch-atomic-clearing primitive without the full feature set (partial fills, * ERC-1271, balance modes, dynamic fees). Audit-grade hardening deferred to Phase 2. */ contract AereSettlement is Ownable, ReentrancyGuard, EIP712 { using SafeERC20 for IERC20; AereVaultRelayer public immutable vaultRelayer; AereSolverRegistry public immutable solverRegistry; /// EIP-712 order struct. /// Buy/sell tokens, amounts, deadline, salt (for replay protection). struct Order { address user; IERC20 sellToken; IERC20 buyToken; uint256 sellAmount; // exact sell amount (fill-or-kill in v1) uint256 minBuyAmount; // user-specified slippage floor uint64 validTo; // unix deadline uint256 salt; // unique-per-user replay nonce } /// What the solver claims it will execute for this order, given the batch's /// uniform clearing price. Must satisfy executedBuyAmount >= order.minBuyAmount. struct Execution { uint256 executedBuyAmount; } bytes32 private constant ORDER_TYPEHASH = keccak256( "Order(address user,address sellToken,address buyToken,uint256 sellAmount,uint256 minBuyAmount,uint64 validTo,uint256 salt)" ); /// Replay protection: orderHash → consumed. mapping(bytes32 => bool) public filled; event Settled(address indexed solver, uint256 orderCount, uint256 timestamp); event OrderFilled( bytes32 indexed orderHash, address indexed user, address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount ); modifier onlySolver() { require(solverRegistry.isSolver(msg.sender), "Settlement: not a solver"); _; } constructor(AereVaultRelayer _vault, AereSolverRegistry _registry) EIP712("AereSettlement", "1") { vaultRelayer = _vault; solverRegistry = _registry; } /// Compute the canonical hash of an order for EIP-712 verification. function orderHash(Order calldata o) public view returns (bytes32) { bytes32 structHash = keccak256(abi.encode( ORDER_TYPEHASH, o.user, address(o.sellToken), address(o.buyToken), o.sellAmount, o.minBuyAmount, o.validTo, o.salt )); return _hashTypedDataV4(structHash); } /** * @notice Settle a batch of orders atomically. * @dev Solver must provide one signature per order and one execution amount per order. * The solver is responsible for sourcing buyToken liquidity (via internal CoW * matching between orders, or by interacting with AereSwap, or by holding its * own inventory). After the batch, every user must have received at least their * minBuyAmount of buyToken. * * Implementation: this contract pulls sellTokens FROM each user TO the solver * via the VaultRelayer, then expects the solver to push buyTokens to each user. * Solver supplies buyTokens from its working capital + post-batch arbitrage. */ function settle( Order[] calldata orders, bytes[] calldata signatures, Execution[] calldata executions ) external onlySolver nonReentrant { require(orders.length == signatures.length && orders.length == executions.length, "Settlement: length mismatch"); require(orders.length > 0, "Settlement: empty batch"); for (uint256 i = 0; i < orders.length; i++) { Order calldata o = orders[i]; require(block.timestamp <= o.validTo, "Settlement: order expired"); require(executions[i].executedBuyAmount >= o.minBuyAmount, "Settlement: below minBuy"); bytes32 h = orderHash(o); require(!filled[h], "Settlement: already filled"); // Verify EIP-712 signature. address signer = ECDSA.recover(h, signatures[i]); require(signer == o.user, "Settlement: bad signature"); filled[h] = true; // 1. Pull sellAmount from user → solver. (VaultRelayer has approval.) vaultRelayer.transferFromUser(o.sellToken, o.user, msg.sender, o.sellAmount); // 2. Solver MUST push executedBuyAmount of buyToken to user. // The settlement contract orchestrates by pulling from solver and pushing to user. // Solver must approve THIS contract for buyToken first (or use internal CoW match // where the buyToken flows from another order's sellToken — handled outside this loop). o.buyToken.safeTransferFrom(msg.sender, o.user, executions[i].executedBuyAmount); emit OrderFilled(h, o.user, address(o.sellToken), address(o.buyToken), o.sellAmount, executions[i].executedBuyAmount); } emit Settled(msg.sender, orders.length, block.timestamp); } /// User can cancel an open order on-chain (rare; mostly orders expire naturally). function cancelOrder(Order calldata o) external { require(msg.sender == o.user, "Settlement: not order owner"); filled[orderHash(o)] = true; // mark consumed → solver can't fill } /// Check whether an order is still openable. function isOrderFillable(Order calldata o) external view returns (bool) { return !filled[orderHash(o)] && block.timestamp <= o.validTo; } }