// 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 {} }