aere-contracts/contracts/bridge/AereERC7683.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

250 lines
9.7 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/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
)
});
}
}