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.
250 lines
9.7 KiB
Solidity
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
|
|
)
|
|
});
|
|
}
|
|
}
|