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.
185 lines
7.7 KiB
Solidity
185 lines
7.7 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @title AereBugBountyVault — Perpetual bug-bounty pool paid in AERE
|
|
* @notice Holds AERE (or WAERE) earmarked for security disclosure rewards.
|
|
* Two roles:
|
|
* - Funder (anyone): deposit AERE into the pool, no withdraw right.
|
|
* - Triager committee (2-of-3 multisig): authorise a payout when a
|
|
* disclosed vulnerability is validated. Payouts go to the
|
|
* reporter's address (passed by the multisig at payout time).
|
|
*
|
|
* @dev Locked design 2026-06-06.
|
|
*
|
|
* Constraints:
|
|
* - Deposits are PERMANENT (no general withdraw — funds are committed
|
|
* to the bounty mission). The triager committee can ONLY release
|
|
* funds via `pay()` which emits a structured PayoutMade event with
|
|
* a finding-ID and amount.
|
|
* - The triager is a multisig EOA passed at deploy and IS replaceable
|
|
* ONLY via a 14-day timelock initiated by itself; no other rotation.
|
|
* (Implemented via proposeTriager + acceptTriager.)
|
|
* - Maximum single payout is capped at MAX_PAYOUT_BPS of current
|
|
* balance, in basis points, set at deploy and immutable. This
|
|
* prevents a compromised triager key from draining the vault in
|
|
* a single call. Default 2000 bps = 20%.
|
|
* - No upgrade proxy. No reset.
|
|
*
|
|
* This is intentionally narrower than a full bounty platform — it is
|
|
* the on-chain SETTLEMENT layer for findings adjudicated off-chain
|
|
* (Immunefi-style with private submission via libsodium sealed-box
|
|
* encrypted to the triager X25519 pubkey on aere.network/bug-bounty).
|
|
*
|
|
* Whitepaper §AereProof v0. Reserve Year-1 disbursement: 4M AERE
|
|
* initial + 3M AERE refill buffer (per immediate_build_plan).
|
|
*/
|
|
contract AereBugBountyVault is ReentrancyGuard {
|
|
|
|
/* ------------------------------- immutable -------------------------------- */
|
|
|
|
/// @notice ERC-20 token in which the vault pays bounties (WAERE).
|
|
address public immutable BOUNTY_TOKEN;
|
|
|
|
/// @notice Max single payout, in bps of current balance.
|
|
uint16 public immutable MAX_PAYOUT_BPS;
|
|
|
|
/// @notice Timelock between proposeTriager and acceptTriager.
|
|
uint256 public constant TRIAGER_TIMELOCK = 14 days;
|
|
|
|
/* --------------------------------- state ---------------------------------- */
|
|
|
|
/// @notice Address that may authorise payouts. Typically a 2-of-3 Foundation
|
|
/// multisig with Ledger-stored keys.
|
|
address public triager;
|
|
|
|
/// @notice Pending replacement triager (zero if no proposal active).
|
|
address public pendingTriager;
|
|
|
|
/// @notice Timestamp at which pendingTriager becomes acceptable.
|
|
uint256 public pendingTriagerEarliestAccept;
|
|
|
|
/// @notice Monotonic counter to map an emitted PayoutMade to a finding.
|
|
/// The triager is responsible for the off-chain finding-ID
|
|
/// (e.g., AERE-VAULT-2026-001) and including it in the call.
|
|
uint256 public payoutNonce;
|
|
|
|
/* --------------------------------- events --------------------------------- */
|
|
|
|
event Funded(address indexed funder, uint256 amount);
|
|
event PayoutMade(
|
|
uint256 indexed nonce,
|
|
bytes32 indexed findingId,
|
|
address indexed reporter,
|
|
uint256 amount
|
|
);
|
|
event TriagerProposed(address indexed proposer, address indexed newTriager, uint256 earliestAccept);
|
|
event TriagerAccepted(address indexed oldTriager, address indexed newTriager);
|
|
event TriagerProposalCancelled(address indexed canceller);
|
|
|
|
/* --------------------------------- errors --------------------------------- */
|
|
|
|
error NotTriager();
|
|
error ZeroAddress();
|
|
error ZeroAmount();
|
|
error AmountExceedsCap();
|
|
error TimelockNotElapsed(uint256 nowTs, uint256 earliest);
|
|
error NoPendingProposal();
|
|
error TransferFailed();
|
|
|
|
/* ------------------------------- modifiers -------------------------------- */
|
|
|
|
modifier onlyTriager() {
|
|
if (msg.sender != triager) revert NotTriager();
|
|
_;
|
|
}
|
|
|
|
/* ------------------------------- constructor ------------------------------ */
|
|
|
|
/**
|
|
* @param _bountyToken WAERE address.
|
|
* @param _triager Initial multisig.
|
|
* @param _maxPayoutBps Max single payout in bps (e.g. 2000 = 20%).
|
|
*/
|
|
constructor(address _bountyToken, address _triager, uint16 _maxPayoutBps) {
|
|
if (_bountyToken == address(0) || _triager == address(0)) revert ZeroAddress();
|
|
require(_maxPayoutBps > 0 && _maxPayoutBps <= 10_000, "bps out of range");
|
|
BOUNTY_TOKEN = _bountyToken;
|
|
triager = _triager;
|
|
MAX_PAYOUT_BPS = _maxPayoutBps;
|
|
}
|
|
|
|
/* ----------------------------------- core --------------------------------- */
|
|
|
|
/// @notice Top up the vault. Permissionless. Once in, AERE is committed.
|
|
function fund(uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
bool ok = IERC20(BOUNTY_TOKEN).transferFrom(msg.sender, address(this), amount);
|
|
if (!ok) revert TransferFailed();
|
|
emit Funded(msg.sender, amount);
|
|
}
|
|
|
|
/**
|
|
* @notice Triager authorises a payout to a reporter for a validated finding.
|
|
* @param findingId Stable identifier the triager committee assigns to the
|
|
* finding off-chain. Public, recorded on-chain.
|
|
* @param reporter Address that receives the bounty.
|
|
* @param amount AERE amount, must be ≤ MAX_PAYOUT_BPS of current balance.
|
|
*/
|
|
function pay(bytes32 findingId, address reporter, uint256 amount) external onlyTriager nonReentrant {
|
|
if (reporter == address(0)) revert ZeroAddress();
|
|
if (amount == 0) revert ZeroAmount();
|
|
|
|
uint256 bal = IERC20(BOUNTY_TOKEN).balanceOf(address(this));
|
|
uint256 cap = bal * uint256(MAX_PAYOUT_BPS) / 10_000;
|
|
if (amount > cap) revert AmountExceedsCap();
|
|
|
|
uint256 n = ++payoutNonce;
|
|
bool ok = IERC20(BOUNTY_TOKEN).transfer(reporter, amount);
|
|
if (!ok) revert TransferFailed();
|
|
emit PayoutMade(n, findingId, reporter, amount);
|
|
}
|
|
|
|
/* ----------------------------- triager rotation --------------------------- */
|
|
|
|
function proposeTriager(address newTriager) external onlyTriager {
|
|
if (newTriager == address(0)) revert ZeroAddress();
|
|
pendingTriager = newTriager;
|
|
pendingTriagerEarliestAccept = block.timestamp + TRIAGER_TIMELOCK;
|
|
emit TriagerProposed(msg.sender, newTriager, pendingTriagerEarliestAccept);
|
|
}
|
|
|
|
function cancelTriagerProposal() external onlyTriager {
|
|
if (pendingTriager == address(0)) revert NoPendingProposal();
|
|
delete pendingTriager;
|
|
delete pendingTriagerEarliestAccept;
|
|
emit TriagerProposalCancelled(msg.sender);
|
|
}
|
|
|
|
function acceptTriager() external onlyTriager {
|
|
if (pendingTriager == address(0)) revert NoPendingProposal();
|
|
if (block.timestamp < pendingTriagerEarliestAccept) {
|
|
revert TimelockNotElapsed(block.timestamp, pendingTriagerEarliestAccept);
|
|
}
|
|
address old = triager;
|
|
triager = pendingTriager;
|
|
delete pendingTriager;
|
|
delete pendingTriagerEarliestAccept;
|
|
emit TriagerAccepted(old, triager);
|
|
}
|
|
|
|
/* ---------------------------------- views --------------------------------- */
|
|
|
|
function balance() external view returns (uint256) {
|
|
return IERC20(BOUNTY_TOKEN).balanceOf(address(this));
|
|
}
|
|
|
|
function singlePayoutCap() external view returns (uint256) {
|
|
uint256 bal = IERC20(BOUNTY_TOKEN).balanceOf(address(this));
|
|
return bal * uint256(MAX_PAYOUT_BPS) / 10_000;
|
|
}
|
|
}
|