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.
253 lines
11 KiB
Solidity
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);
|
|
}
|