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

253 lines
11 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 "../stablecoins/IHypMailbox.sol";
/**
* @title AereWarpRouteV3
* @notice Hyperlane v3 TokenRouter for USDC.e on AERE chain 2800, bound to the
* WORKING AereMailboxV3 (0x7BF113Ab1BCd2b6da01804764065776e3057605a) and
* its default post-dispatch hook AereIGPV3
* (0x5e4B8e9b196B1c7b3Be86148769aB0047c79744c).
*
* This REPLACES the dead legacy path. The prior AereWarpRoute pointed at
* AereMessenger, whose runtime bytecode has NO quoteDispatch selector, so
* every quote reverted and the route was unusable. AereMailboxV3 exposes
* a real quoteDispatch(uint32,bytes32,bytes) and routes the quoted fee to
* AereIGPV3 internally on dispatch, so this router works end to end on the
* AERE side.
*
* TOKEN HANDLING — two immutable modes, chosen at deploy:
*
* Mode.MintBurn (canonical synthetic; USDC.e is the bridged representation):
* - transferRemote burns the caller's USDC.e (TOKEN.burnFrom), then dispatches.
* - handle mints USDC.e to the recipient (TOKEN.mint).
* Use this ONLY when the existing USDC.e grants THIS router mint/burn rights
* (i.e. its immutable BRIDGE == this router). No NEW token is minted here;
* USDC.e is the existing bridged-collateral representation, 1:1 backed by the
* native USDC locked in the sister AereHypERC20Collateral on Ethereum.
*
* Mode.Lock (lock/release against an existing token this router does NOT
* control the mint of):
* - transferRemote pulls (locks) the existing token via safeTransferFrom.
* - handle releases the locked token via safeTransfer to the recipient.
*
* WIRE FORMAT: Hyperlane HypERC20 convention — body = abi.encodePacked(
* bytes32 recipient, uint256 amount) = exactly 64 bytes.
*
* V3 FEE FLOW: dispatch() forwards the quoted fee to the mailbox's default hook
* (the IGP). Callers pay the fee as msg.value to transferRemote; anything above
* the quote is refunded to the caller. quoteDispatch on the mailbox already
* includes the IGP hook price, so this router quotes against the mailbox (the
* canonical v3 flow) and keeps an immutable IGP reference only for the
* transparency view quoteGasPayment().
*
* NO new ERC-20 / wrapped / governance token is introduced by this contract.
*/
contract AereWarpRouteV3 is Ownable, ReentrancyGuard, IHypMessageRecipient {
using SafeERC20 for IERC20;
using TypeCasts for address;
using TypeCasts for bytes32;
enum Mode { Lock, MintBurn }
/* --------------------------------- immutables --------------------------------- */
/// @notice AereMailboxV3 — dispatch + quoteDispatch endpoint on chain 2800.
IHypMailbox public immutable MAILBOX;
/// @notice AereIGPV3 — the mailbox default post-dispatch hook (fee sink).
/// Referenced for the quoteGasPayment() transparency view only; the
/// authoritative fee for a dispatch is MAILBOX.quoteDispatch(...).
IAereIGPV3View public immutable IGP;
/// @notice Existing USDC.e representation on AERE (or the token this router
/// locks/releases). NEVER a token minted by this contract's deploy.
IERC20 public immutable TOKEN;
/// @notice Lock (release existing) or MintBurn (mint/burn existing USDC.e).
Mode public immutable MODE;
/* ----------------------------------- state ------------------------------------ */
/// @notice Hyperlane domain id => trusted remote router (bytes32-padded address).
mapping(uint32 => bytes32) public routers;
uint256 public totalSentOut;
uint256 public totalReceivedIn;
/* ----------------------------------- events ----------------------------------- */
event RemoteRouterEnrolled(uint32 indexed domain, bytes32 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 InsufficientGasPayment(uint256 paid, uint256 required);
error ZeroAmount();
error ZeroRecipient();
error RefundFailed();
/* --------------------------------- modifiers ---------------------------------- */
modifier onlyMailbox() {
if (msg.sender != address(MAILBOX)) revert NotMailbox();
_;
}
/* -------------------------------- constructor --------------------------------- */
/**
* @param mailbox AereMailboxV3 address (must be non-zero).
* @param igp AereIGPV3 address (mailbox default hook). May be zero if the
* operator does not want the on-chain transparency view; the
* dispatch fee path never depends on it.
* @param token Existing USDC.e (MintBurn) or existing token to lock (Lock).
* @param mode Lock or MintBurn.
*/
constructor(address mailbox, address igp, address token, Mode mode) {
require(mailbox != address(0) && token != address(0), "zero address");
MAILBOX = IHypMailbox(mailbox);
IGP = IAereIGPV3View(igp);
TOKEN = IERC20(token);
MODE = mode;
}
/* ------------------------------- router enroll -------------------------------- */
/// @notice Bind the single trusted remote router for a domain (owner-gated).
function enrollRemoteRouter(uint32 domain, bytes32 router) external onlyOwner {
routers[domain] = router;
emit RemoteRouterEnrolled(domain, router);
}
/// @notice Remove the trusted remote router for a domain (owner-gated).
function unenrollRemoteRouter(uint32 domain) external onlyOwner {
delete routers[domain];
emit RemoteRouterEnrolled(domain, bytes32(0));
}
/* ----------------------------------- outbound --------------------------------- */
/**
* @notice Pull `amount` of TOKEN from the caller and dispatch a Hyperlane v3
* message instructing the enrolled router on `destination` to release
* (or mint) the corresponding asset to `recipient`.
* @param destination Hyperlane domain id of the remote chain.
* @param recipient bytes32-padded recipient address on the remote chain.
* @param amount Token units to send (USDC.e has 6 decimals).
* @return messageId The dispatched Hyperlane v3 message id.
*/
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);
// Authoritative fee for the v3 dispatch. The mailbox forwards this to the
// IGP hook internally, so we send exactly this as msg.value to dispatch().
uint256 fee = MAILBOX.quoteDispatch(destination, remoteRouter, body);
if (msg.value < fee) revert InsufficientGasPayment(msg.value, fee);
// Pull the caller's tokens BEFORE the cross-chain dispatch.
if (MODE == Mode.MintBurn) {
IWarpMintBurn(address(TOKEN)).burnFrom(msg.sender, amount);
} else {
TOKEN.safeTransferFrom(msg.sender, address(this), amount);
}
messageId = MAILBOX.dispatch{value: fee}(destination, remoteRouter, body);
// Refund any msg.value above the quoted fee to the caller.
if (msg.value > fee) {
(bool ok, ) = msg.sender.call{value: msg.value - fee}("");
if (!ok) revert RefundFailed();
}
totalSentOut += amount;
emit SentTransferRemote(destination, recipient, amount, messageId);
}
/* ----------------------------------- inbound ---------------------------------- */
/**
* @notice Called by AereMailboxV3 after its validator set verified the
* message. Releases (Lock) or mints (MintBurn) `amount` to the
* recipient parsed from the 64-byte Hyperlane body.
*/
function handle(
uint32 origin,
bytes32 sender,
bytes calldata body
) external payable override onlyMailbox nonReentrant {
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();
if (MODE == Mode.MintBurn) {
IWarpMintBurn(address(TOKEN)).mint(recipient, amount);
} else {
TOKEN.safeTransfer(recipient, amount);
}
totalReceivedIn += amount;
emit ReceivedTransferRemote(origin, recipient, amount);
}
/* ------------------------------------ views ----------------------------------- */
/// @notice Fee that transferRemote(destination, recipient, amount) requires as
/// msg.value, quoted against AereMailboxV3 (includes the IGP hook).
function quoteTransferRemote(
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 Transparency view straight off AereIGPV3 for a given destination
/// and gas limit. Reverts if IGP was not set at deploy.
function quoteGasPayment(uint32 destination, uint256 gasLimit) external view returns (uint256) {
return IGP.quoteGasPayment(destination, gasLimit);
}
/// @notice Locked balance held by this router (Lock mode reserve view).
function lockedSupply() external view returns (uint256) {
return TOKEN.balanceOf(address(this));
}
}
/* ------------------------------- helper interfaces -------------------------------- */
/// @notice USDC.e mint/burn surface (matches contracts/stablecoins/USDCe.sol).
interface IWarpMintBurn {
function mint(address to, uint256 amount) external;
function burnFrom(address from, uint256 amount) external;
}
/// @notice AereIGPV3 quoting surface for the transparency view.
interface IAereIGPV3View {
function quoteGasPayment(uint32 destinationDomain, uint256 gasLimit) external view returns (uint256);
}