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.
151 lines
6.4 KiB
Solidity
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 {}
|
|
}
|