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.
181 lines
6.8 KiB
Solidity
181 lines
6.8 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 "./IHypMailbox.sol";
|
|
|
|
/**
|
|
* @title AereHypERC20Collateral — Ethereum-mainnet lockbox for Phase-1
|
|
* wrapped stablecoins bridged to AERE.
|
|
*
|
|
* @notice Deployed on Ethereum mainnet (chain 1) per bridged asset:
|
|
* - USDC mainnet → AereHypERC20Collateral(USDC) ↔ USDC.e on AERE
|
|
* - USDT mainnet → AereHypERC20Collateral(USDT) ↔ USDT.e on AERE
|
|
* - USDe mainnet → AereHypERC20Collateral(USDe) ↔ USDe.e on AERE
|
|
* - EURC mainnet → AereHypERC20Collateral(EURC) ↔ EURC.e on AERE
|
|
*
|
|
* @dev Outbound (lock + dispatch):
|
|
* User approves N native-USDC and calls transferRemote(destDomain=2800,
|
|
* recipient, N). Contract pulls N USDC into its lockbox, dispatches a
|
|
* Hyperlane message to the AERE synthetic router with [recipient | N].
|
|
*
|
|
* Inbound (release):
|
|
* Mailbox calls handle(originDomain=2800, senderBytes32, body). Body is
|
|
* [recipient | amount]. Contract releases `amount` of native USDC to
|
|
* `recipient`. Sender must match the enrolled AERE router.
|
|
*
|
|
* Reserve invariant: the contract's native-USDC balance equals the
|
|
* outstanding USDC.e supply on AERE (modulo in-flight messages). The
|
|
* AereNavOracle (Q3 2026) snapshots both sides nightly for public
|
|
* attestation.
|
|
*
|
|
* Trusted-router enrolment, IGP fees, message format and admin model
|
|
* mirror the AERE-side synthetic exactly.
|
|
*/
|
|
contract AereHypERC20Collateral is Ownable, ReentrancyGuard, IHypMessageRecipient {
|
|
using TypeCasts for address;
|
|
using TypeCasts for bytes32;
|
|
|
|
/* --------------------------------- immutables --------------------------------- */
|
|
|
|
IHypMailbox public immutable MAILBOX;
|
|
IERC20 public immutable COLLATERAL_TOKEN; // native USDC / USDT / USDe / EURC
|
|
|
|
/* ----------------------------------- state ------------------------------------ */
|
|
|
|
mapping(uint32 => bytes32) public routers;
|
|
|
|
/* ----------------------------------- events ----------------------------------- */
|
|
|
|
event RouterEnrolled(uint32 indexed domain, bytes32 indexed router);
|
|
event SentTransferRemote(
|
|
uint32 indexed destination,
|
|
bytes32 indexed recipient,
|
|
uint256 amount,
|
|
bytes32 messageId
|
|
);
|
|
event ReceivedTransferRemote(
|
|
uint32 indexed origin,
|
|
address indexed recipient,
|
|
uint256 amount
|
|
);
|
|
|
|
/* ----------------------------------- errors ----------------------------------- */
|
|
|
|
error NotMailbox();
|
|
error UnknownRouter(uint32 origin, bytes32 sender);
|
|
error NoRouterEnrolled(uint32 destination);
|
|
error InsufficientIgpPayment(uint256 paid, uint256 required);
|
|
error ZeroAmount();
|
|
error ZeroRecipient();
|
|
error PullFailed();
|
|
error PushFailed();
|
|
error RefundFailed();
|
|
|
|
/* --------------------------------- modifiers --------------------------------- */
|
|
|
|
modifier onlyMailbox() {
|
|
if (msg.sender != address(MAILBOX)) revert NotMailbox();
|
|
_;
|
|
}
|
|
|
|
/* -------------------------------- constructor ------------------------------- */
|
|
|
|
constructor(address mailbox, address collateralToken) {
|
|
require(mailbox != address(0) && collateralToken != address(0), "zero address");
|
|
MAILBOX = IHypMailbox(mailbox);
|
|
COLLATERAL_TOKEN = IERC20(collateralToken);
|
|
}
|
|
|
|
/* ------------------------------- router enroll ------------------------------ */
|
|
|
|
function enrollRouter(uint32 domain, bytes32 router) external onlyOwner {
|
|
routers[domain] = router;
|
|
emit RouterEnrolled(domain, router);
|
|
}
|
|
|
|
function unenrollRouter(uint32 domain) external onlyOwner {
|
|
delete routers[domain];
|
|
emit RouterEnrolled(domain, bytes32(0));
|
|
}
|
|
|
|
/* ----------------------------------- outbound ------------------------------- */
|
|
|
|
/**
|
|
* @notice Lock `amount` of COLLATERAL_TOKEN from caller and dispatch a
|
|
* mint instruction to the enrolled router on the destination.
|
|
*/
|
|
function transferRemote(
|
|
uint32 destination,
|
|
bytes32 recipient,
|
|
uint256 amount
|
|
) external payable nonReentrant returns (bytes32 messageId) {
|
|
if (amount == 0) revert ZeroAmount();
|
|
if (recipient == bytes32(0)) revert ZeroRecipient();
|
|
bytes32 remoteRouter = routers[destination];
|
|
if (remoteRouter == bytes32(0)) revert NoRouterEnrolled(destination);
|
|
|
|
bytes memory body = abi.encodePacked(recipient, amount);
|
|
|
|
uint256 fee = MAILBOX.quoteDispatch(destination, remoteRouter, body);
|
|
if (msg.value < fee) revert InsufficientIgpPayment(msg.value, fee);
|
|
|
|
// Lock the collateral.
|
|
bool pulled = COLLATERAL_TOKEN.transferFrom(msg.sender, address(this), amount);
|
|
if (!pulled) revert PullFailed();
|
|
|
|
messageId = MAILBOX.dispatch{value: fee}(destination, remoteRouter, body);
|
|
|
|
if (msg.value > fee) {
|
|
(bool ok, ) = msg.sender.call{value: msg.value - fee}("");
|
|
if (!ok) revert RefundFailed();
|
|
}
|
|
|
|
emit SentTransferRemote(destination, recipient, amount, messageId);
|
|
}
|
|
|
|
/* ----------------------------------- inbound -------------------------------- */
|
|
|
|
function handle(
|
|
uint32 origin,
|
|
bytes32 sender,
|
|
bytes calldata body
|
|
) external payable override onlyMailbox {
|
|
bytes32 expected = routers[origin];
|
|
if (expected == bytes32(0) || expected != sender) revert UnknownRouter(origin, sender);
|
|
|
|
require(body.length == 64, "invalid body length");
|
|
bytes32 recipientB32;
|
|
uint256 amount;
|
|
assembly {
|
|
recipientB32 := calldataload(body.offset)
|
|
amount := calldataload(add(body.offset, 32))
|
|
}
|
|
address recipient = recipientB32.bytes32ToAddress();
|
|
|
|
bool ok = COLLATERAL_TOKEN.transfer(recipient, amount);
|
|
if (!ok) revert PushFailed();
|
|
|
|
emit ReceivedTransferRemote(origin, recipient, amount);
|
|
}
|
|
|
|
/* ----------------------------------- views ---------------------------------- */
|
|
|
|
function quoteDispatchFee(
|
|
uint32 destination,
|
|
bytes32 recipient,
|
|
uint256 amount
|
|
) external view returns (uint256 fee) {
|
|
bytes memory body = abi.encodePacked(recipient, amount);
|
|
return MAILBOX.quoteDispatch(destination, routers[destination], body);
|
|
}
|
|
|
|
/// @notice Lockbox balance — should equal outstanding USDC.e supply on AERE
|
|
/// (modulo in-flight Hyperlane messages).
|
|
function lockedSupply() external view returns (uint256) {
|
|
return COLLATERAL_TOKEN.balanceOf(address(this));
|
|
}
|
|
}
|