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