aere-contracts/contracts/dex/AereSettlement.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

164 lines
6.9 KiB
Solidity

// 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;
}
}