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.
164 lines
6.9 KiB
Solidity
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;
|
|
}
|
|
}
|