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.
120 lines
5.2 KiB
Solidity
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);
|
|
}
|
|
}
|