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

120 lines
5.2 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "./AereMessenger.sol";
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
function mint(address to, uint256 amount) external;
function burn(address from, uint256 amount) external;
}
/**
* @title AereWarpRoute
* @notice Typed cross-chain token bridge built on AereMessenger.
* One AereWarpRoute per token per chain. Two flavours:
*
* Collateral mode (token has natural liquidity on this chain):
* - transferRemote: lock tokens on this chain → emit message → mint on remote
* - handle (incoming): receive message → unlock tokens on this chain to recipient
*
* Synthetic mode (token is a bridged representation):
* - transferRemote: burn synthetic tokens here → emit message → mint on remote
* - handle (incoming): receive message → mint synthetic tokens here to recipient
*
* This contract pattern mirrors Hyperlane Warp Route exactly, so apps built
* against Hyperlane's tooling work on AERE with only the contract address change.
*
* For Bank28: USDC.aere, USDT.aere, WETH.aere, cbBTC.aere are all separate
* AereWarpRoute deployments in collateral mode (on the origin chain) and
* synthetic mode (on AERE) — totalling 8 deployments for 4 assets across
* 2 chain pairs.
*/
contract AereWarpRoute is IMessageRecipient, Ownable, ReentrancyGuard {
enum Mode { Collateral, Synthetic }
AereMessenger public immutable messenger;
IERC20 public immutable token;
Mode public immutable mode;
/// Remote AereWarpRoute deployments per destination domain.
/// bytes32 because Hyperlane convention is to pad addresses.
mapping(uint32 => bytes32) public remoteRouter;
/// Lifetime metrics for transparency.
uint256 public totalSentOut;
uint256 public totalReceivedIn;
event SentTransferRemote(uint32 indexed destDomain, bytes32 indexed recipient, uint256 amount, bytes32 messageId);
event ReceivedTransferRemote(uint32 indexed origin, address indexed recipient, uint256 amount);
event RemoteRouterEnrolled(uint32 indexed domain, bytes32 router);
constructor(AereMessenger _messenger, IERC20 _token, Mode _mode) {
messenger = _messenger;
token = _token;
mode = _mode;
}
// ───────────────────── Admin ─────────────────────
/// Bind a remote AereWarpRoute deployment so it can talk to us.
function enrollRemoteRouter(uint32 domain, bytes32 router) external onlyOwner {
remoteRouter[domain] = router;
emit RemoteRouterEnrolled(domain, router);
}
// ───────────────────── Outbound ─────────────────────
/**
* @notice Send tokens to a recipient on another chain via Hyperlane-compatible bridging.
* @dev User must have approved this contract for `amount` of `token` first.
*/
function transferRemote(
uint32 destDomain,
bytes32 recipient,
uint256 amount
) external payable nonReentrant returns (bytes32 messageId) {
require(remoteRouter[destDomain] != bytes32(0), "WarpRoute: no remote router");
require(amount > 0, "WarpRoute: zero amount");
if (mode == Mode.Collateral) {
// Pull tokens; they remain locked here until a withdrawal arrives.
require(token.transferFrom(msg.sender, address(this), amount), "WarpRoute: pull failed");
} else {
// Burn synthetic tokens from the sender — the remote side will mint the original.
token.burn(msg.sender, amount);
}
bytes memory body = abi.encode(recipient, amount);
messageId = messenger.dispatch{ value: msg.value }(destDomain, remoteRouter[destDomain], body);
totalSentOut += amount;
emit SentTransferRemote(destDomain, recipient, amount, messageId);
}
// ───────────────────── Inbound (only callable by Messenger) ─────────────────────
function handle(uint32 origin, bytes32 sender, bytes calldata body) external payable override {
require(msg.sender == address(messenger), "WarpRoute: caller not messenger");
require(sender == remoteRouter[origin], "WarpRoute: unknown remote router");
(bytes32 recipient32, uint256 amount) = abi.decode(body, (bytes32, uint256));
address recipient = address(uint160(uint256(recipient32)));
if (mode == Mode.Collateral) {
// Release locked tokens to recipient.
require(token.transfer(recipient, amount), "WarpRoute: release failed");
} else {
// Mint synthetic tokens to recipient.
token.mint(recipient, amount);
}
totalReceivedIn += amount;
emit ReceivedTransferRemote(origin, recipient, amount);
}
}