aere-contracts/contracts/bridge/AereWarpRoute.sol
Aere Network acac2f00a6
Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The unpublished line of work joins the sanitized public line
The published line and the local line had no common ancestor: the public one
carried the redaction pass, the local one carried three weeks of corrections
that never shipped. This commit ports the local work onto the public line,
keeps every public redaction, and extends the same discretion to seven client
mentions that were still named in published comments.

Carried: LICENSE year and LICENSING.md; the measured burn figures replacing
the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is
a share of validator coinbase revenue, which is zero today); 'audited' removed
from next to Bouncy Castle; citation paths rewritten to published form with
CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and
ownership comments brought down to what the code does; the AerePyth repair;
the shutter test helper the tests cite; runnable package.json entries; the CI
file split into a GitHub/Gitea twin pair with a real measured test-run status;
and the .gitignore hardening written after a compiled artifact leaked a local
path in a sibling repository. A false '2-of-3 multisig' description of the
owner account is corrected to what the chain measures: an externally owned
account. The self-audit findings catalog stays unpublished pending an explicit
decision.
2026-08-15 13:59:30 +03:00

120 lines
5.3 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 a card program: 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);
}
}