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

151 lines
6.4 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* @title AereBridge
* @notice Federated lock-and-release bridge for native AERE between AERE Network
* and external EVM chains (Ethereum, BSC, Polygon, etc — 75+ supported).
* @dev Lock side: users deposit AERE here; off-chain federation observes the
* Locked event and mints wrapped-AERE on the destination chain.
* Release side: the federation attests to a destination-chain burn by signing
* (nonce, recipient, amount, srcChainId). Once `threshold` signatures from
* authorized signers are presented, the bridge releases AERE on this chain.
*
* Replay protection: each (srcChainId, nonce) pair can only be released once.
* Each signer can only contribute one signature per release (enforced via
* sorted-address requirement).
*/
contract AereBridge is Ownable, ReentrancyGuard, Pausable {
using ECDSA for bytes32;
uint256 public constant MIN_DEPOSIT = 1 ether; // 1 AERE
uint256 public constant MAX_SIGNERS = 21;
address[] public signers;
mapping(address => bool) public isSigner;
uint256 public threshold;
uint256 public depositNonce;
mapping(uint256 => mapping(uint256 => bool)) public processed; // srcChainId => srcNonce => done
event SignerAdded(address indexed signer);
event SignerRemoved(address indexed signer);
event ThresholdUpdated(uint256 newThreshold);
event Locked(uint256 indexed nonce, address indexed sender, uint256 indexed dstChainId, address dstRecipient, uint256 amount);
event Released(uint256 indexed srcChainId, uint256 indexed srcNonce, address indexed recipient, uint256 amount);
constructor(address[] memory initialSigners, uint256 _threshold) {
require(initialSigners.length > 0 && initialSigners.length <= MAX_SIGNERS, "signers");
require(_threshold > 0 && _threshold <= initialSigners.length, "threshold");
for (uint256 i = 0; i < initialSigners.length; i++) {
address s = initialSigners[i];
require(s != address(0) && !isSigner[s], "bad signer");
isSigner[s] = true;
signers.push(s);
emit SignerAdded(s);
}
threshold = _threshold;
emit ThresholdUpdated(_threshold);
}
// ── Lock (outbound) ─────────────────────────────────────────
function lock(uint256 dstChainId, address dstRecipient) external payable whenNotPaused nonReentrant {
require(msg.value >= MIN_DEPOSIT, "min");
require(dstRecipient != address(0), "recipient");
uint256 n = ++depositNonce;
emit Locked(n, msg.sender, dstChainId, dstRecipient, msg.value);
}
// ── Release (inbound) ───────────────────────────────────────
/**
* @param srcChainId chain ID of the source chain where the burn occurred
* @param srcNonce bridge-issued nonce on the source chain (replay key)
* @param recipient AERE Network address to receive funds
* @param amount amount of AERE to release
* @param signatures array of ECDSA signatures over the canonical message,
* ordered by signer address ascending (deduplication)
*/
function release(
uint256 srcChainId,
uint256 srcNonce,
address recipient,
uint256 amount,
bytes[] calldata signatures
) external nonReentrant whenNotPaused {
require(!processed[srcChainId][srcNonce], "processed");
require(signatures.length >= threshold, "threshold");
require(amount > 0 && recipient != address(0), "params");
require(address(this).balance >= amount, "liquidity");
bytes32 message = keccak256(
abi.encodePacked(
address(this),
block.chainid,
srcChainId,
srcNonce,
recipient,
amount
)
).toEthSignedMessageHash();
address last = address(0);
for (uint256 i = 0; i < signatures.length; i++) {
address recovered = message.recover(signatures[i]);
require(isSigner[recovered], "not signer");
require(recovered > last, "unsorted/dup");
last = recovered;
}
processed[srcChainId][srcNonce] = true;
(bool ok, ) = recipient.call{value: amount}("");
require(ok, "transfer");
emit Released(srcChainId, srcNonce, recipient, amount);
}
// ── Federation management ───────────────────────────────────
function addSigner(address s) external onlyOwner {
require(s != address(0) && !isSigner[s], "bad");
require(signers.length < MAX_SIGNERS, "max");
isSigner[s] = true;
signers.push(s);
emit SignerAdded(s);
}
function removeSigner(address s) external onlyOwner {
require(isSigner[s], "not signer");
isSigner[s] = false;
for (uint256 i = 0; i < signers.length; i++) {
if (signers[i] == s) {
signers[i] = signers[signers.length - 1];
signers.pop();
break;
}
}
if (threshold > signers.length) {
threshold = signers.length;
emit ThresholdUpdated(threshold);
}
emit SignerRemoved(s);
}
function setThreshold(uint256 newThreshold) external onlyOwner {
require(newThreshold > 0 && newThreshold <= signers.length, "range");
threshold = newThreshold;
emit ThresholdUpdated(newThreshold);
}
function signerCount() external view returns (uint256) { return signers.length; }
// ── Emergency ───────────────────────────────────────────────
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
/// @notice Allow the foundation to seed liquidity for releases.
receive() external payable {}
}