aere-contracts/contracts/stablecoins/AereHypERC20Collateral.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

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