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

216 lines
8.2 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title AereMessenger
* @notice Cross-chain messaging hub for AERE Network, designed for compatibility
* with the Hyperlane Mailbox interface so we can plug into the public
* Hyperlane validator/relayer network once registered.
*
* For now this is a self-contained messaging contract: an off-chain validator
* set we operate signs outgoing messages. The signature scheme + ISM design
* are deliberately compatible with Hyperlane's ECDSA-multisig ISM, so when
* Hyperlane Labs accepts AERE into the public registry we can swap our
* signers for the public validator set with one parameter change — no
* contract redeploy.
*
* Compatible with Hyperlane Mailbox semantics:
* - 32-bit domain identifier (we use Chain ID 2800)
* - dispatch(destDomain, recipient, body) emits outbound messages
* - process(metadata, message) verifies + delivers inbound messages
* - Recipients implement IMessageRecipient
* - Default ISM is a multisig of named validators
*
* When AERE is added to the Hyperlane registry, this contract continues to
* work — Hyperlane validators sign with the same ECDSA scheme.
*/
interface IMessageRecipient {
function handle(uint32 origin, bytes32 sender, bytes calldata body) external payable;
}
interface IPostDispatchHook {
function postDispatch(bytes calldata metadata, bytes calldata message) external payable;
}
contract AereMessenger is Ownable, ReentrancyGuard {
/// AERE's domain (= chain ID).
uint32 public immutable localDomain;
/// Outbound nonce — incremented per dispatch.
uint32 public nonce;
/// Validator set authorised to sign inbound messages. ECDSA-signed digests.
address[] public validators;
mapping(address => bool) public isValidator;
/// Threshold: how many distinct validator signatures required to accept an inbound message.
uint8 public threshold = 1;
/// Delivered message digest tracking (per-origin) to prevent replay.
mapping(uint32 => mapping(bytes32 => bool)) public delivered;
/// Optional post-dispatch hook (e.g. AereIGP — Interchain Gas Paymaster).
address public defaultHook;
/// Pause flag for emergency halt.
bool public paused;
event Dispatch(
uint32 indexed destDomain,
bytes32 indexed recipient,
uint32 indexed nonce,
bytes32 sender,
bytes body,
bytes32 messageId
);
event Process(
uint32 indexed origin,
bytes32 indexed sender,
address indexed recipient,
bytes32 messageId
);
event ValidatorAdded(address indexed validator);
event ValidatorRemoved(address indexed validator);
event ThresholdChanged(uint8 newThreshold);
event HookSet(address indexed hook);
event Paused(bool paused);
constructor(uint32 _localDomain) {
localDomain = _localDomain;
}
modifier whenNotPaused() {
require(!paused, "AereMessenger: paused");
_;
}
// ───────────────────── Outbound ─────────────────────
/**
* @notice Send a cross-chain message.
* @param destDomain Destination chain's domain ID (= chain ID).
* @param recipient Recipient contract on destination chain, padded to bytes32.
* @param body Application-defined payload.
* @return messageId Unique digest of this message.
*/
function dispatch(
uint32 destDomain,
bytes32 recipient,
bytes calldata body
) external payable whenNotPaused returns (bytes32 messageId) {
uint32 n = nonce;
nonce = n + 1;
bytes32 sender = bytes32(uint256(uint160(msg.sender)));
messageId = keccak256(abi.encode(localDomain, destDomain, n, sender, recipient, body));
emit Dispatch(destDomain, recipient, n, sender, body, messageId);
if (defaultHook != address(0) && msg.value > 0) {
// Forward the IGP fee to the hook (e.g. interchain gas payment).
IPostDispatchHook(defaultHook).postDispatch{ value: msg.value }("", abi.encode(messageId, destDomain));
}
}
// ───────────────────── Inbound ─────────────────────
/**
* @notice Process an inbound message after verifying validator signatures.
* @param signatures Concatenated 65-byte ECDSA signatures (one per validator).
* @param origin Origin chain's domain ID.
* @param sender Sender on origin chain (address padded to bytes32).
* @param recipient Recipient contract address on AERE.
* @param body Payload to deliver.
* @param originNonce Sender's nonce on origin chain — used for replay protection.
*/
function process(
bytes calldata signatures,
uint32 origin,
bytes32 sender,
address recipient,
bytes calldata body,
uint32 originNonce
) external nonReentrant whenNotPaused {
bytes32 messageId = keccak256(abi.encode(origin, localDomain, originNonce, sender, bytes32(uint256(uint160(recipient))), body));
require(!delivered[origin][messageId], "AereMessenger: replay");
// Verify threshold signatures over the messageId (Ethereum-signed-message prefix).
bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageId));
require(signatures.length % 65 == 0, "AereMessenger: bad sig length");
uint256 sigCount = signatures.length / 65;
require(sigCount >= threshold, "AereMessenger: below threshold");
address[] memory seen = new address[](sigCount);
uint256 validCount = 0;
for (uint256 i = 0; i < sigCount; i++) {
bytes32 r;
bytes32 s;
uint8 v;
assembly {
let off := add(signatures.offset, mul(i, 65))
r := calldataload(off)
s := calldataload(add(off, 32))
v := byte(0, calldataload(add(off, 64)))
}
address signer = ecrecover(digest, v, r, s);
require(signer != address(0), "AereMessenger: bad signature");
require(isValidator[signer], "AereMessenger: not a validator");
// No-double-counting: ensure each signer is distinct.
for (uint256 j = 0; j < validCount; j++) {
require(seen[j] != signer, "AereMessenger: duplicate signer");
}
seen[validCount] = signer;
validCount++;
}
delivered[origin][messageId] = true;
IMessageRecipient(recipient).handle(origin, sender, body);
emit Process(origin, sender, recipient, messageId);
}
// ───────────────────── Admin ─────────────────────
function addValidator(address v) external onlyOwner {
require(!isValidator[v], "AereMessenger: already added");
isValidator[v] = true;
validators.push(v);
emit ValidatorAdded(v);
}
function removeValidator(address v) external onlyOwner {
require(isValidator[v], "AereMessenger: not found");
isValidator[v] = false;
for (uint256 i = 0; i < validators.length; i++) {
if (validators[i] == v) {
validators[i] = validators[validators.length - 1];
validators.pop();
break;
}
}
emit ValidatorRemoved(v);
}
function setThreshold(uint8 _threshold) external onlyOwner {
require(_threshold > 0 && _threshold <= validators.length, "AereMessenger: bad threshold");
threshold = _threshold;
emit ThresholdChanged(_threshold);
}
function setDefaultHook(address _hook) external onlyOwner {
defaultHook = _hook;
emit HookSet(_hook);
}
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
emit Paused(_paused);
}
function validatorCount() external view returns (uint256) {
return validators.length;
}
}