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