// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

/**
 * @title sAERE — Liquid staking receipt for AERE
 * @notice ERC-4626 receipt vault. Depositors lock WAERE, receive sAERE. The
 *         WAERE / sAERE exchange rate drifts UP as the protocol routes a portion
 *         of chain fees into this vault via AereSink.
 *
 * @dev    Locked design 2026-06-06 (post Round 5 hardening 2026-06-09).
 *
 *         AUDIT FIX R5 (HIGH-1): yield is no longer recognised as a step
 *         function the moment AereSink transfers AERE in. Instead the
 *         arrivals are folded into a "reward reserve" that vests linearly
 *         over DRIP_DURATION (7 days). This kills the public-mempool
 *         flash-loan sandwich (deposit → flush → redeem) by ensuring the
 *         share price cannot jump in a single block.
 *
 *         AUDIT FIX R5 (HIGH-4): _decimalsOffset() is now overridden to 6 and
 *         the dead-share seed raised from 1000 wei to 1 WAERE. Together this
 *         pushes the cost of a successful inflation attack out of feasibility
 *         (donor would need to spend > 1e18 × 1e6 AERE per 1-wei share gain).
 *
 *         SEC Aug 5 2025 receipt-token safe-harbor still applies — drip is
 *         not a fee, it's deterministic vesting of revenue inflows. Provider
 *         does not choose whether/when/how much; once funds arrive they
 *         vest mechanically.
 *
 *         The contract still has: NO admin / NO owner / NO pause / NO upgrade
 *         proxy / NO parameter setters. DRIP_DURATION + decimal offset are
 *         compile-time constants. The sync() entrypoint is permissionless
 *         and is only a state observer — it cannot move funds out.
 */
contract sAERE is ERC4626 {
    /// @notice Linear vesting horizon for reward arrivals. 7 days.
    uint256 public constant DRIP_DURATION = 7 days;

    /// @notice AUDIT FIX R6 (HIGH): minimum arrivals (as bps of current
    /// undistributed reserve) required to RESTART the drip period. Below
    /// this threshold, arrivals only top up the existing rewardRate without
    /// extending periodFinish — preventing a 1-wei-per-block griefer from
    /// indefinitely starving stakers.
    uint256 public constant DRIP_RESTART_THRESHOLD_BPS = 100; // 1%

    /// @notice Dead-share seed kept at 1000 wei to preserve deployer UX; the
    ///         real inflation moat now comes from _decimalsOffset()=6 which
    ///         widens the share/asset ratio by 1e6 — strictly dominant.
    uint256 public constant DEAD_SHARES_SEED_WAERE = 1000 wei;

    address public constant DEAD_ADDRESS = 0x000000000000000000000000000000000000dEaD;

    /// @notice AERE per second currently being released into totalAssets.
    uint256 public rewardRate;
    /// @notice Unix timestamp when the current drip period ends.
    uint256 public periodFinish;
    /// @notice Cached balance at the last sync — distinguishes deposits from
    ///         reward arrivals on the next sync.
    uint256 public lastObservedBalance;

    event RewardArrived(uint256 amount, uint256 newRewardRate, uint256 newPeriodFinish);

    constructor(IERC20 waereToken)
        ERC20("Staked AERE", "sAERE")
        ERC4626(waereToken)
    {
        SafeERC20Like.safeTransferFrom(
            waereToken,
            msg.sender,
            address(this),
            DEAD_SHARES_SEED_WAERE
        );
        _mint(DEAD_ADDRESS, DEAD_SHARES_SEED_WAERE);
        // Initialise the observed balance so the first sync doesn't classify
        // the dead-seed as a reward arrival.
        lastObservedBalance = DEAD_SHARES_SEED_WAERE;
    }

    /* ----------------------- inflation-attack widening ----------------------- */

    /// @notice AUDIT FIX R5 (HIGH-4): override OZ default 0 → 6. Combined with
    ///         the 1 WAERE dead-seed this widens the inflation moat by ~1e15×.
    function _decimalsOffset() internal pure override returns (uint8) {
        return 6;
    }

    /* ------------------------------ drip math ------------------------------ */

    /// @dev AERE still locked in the reward reserve (not yet vested).
    function _undistributedNow() internal view returns (uint256) {
        if (block.timestamp >= periodFinish) return 0;
        uint256 remaining = periodFinish - block.timestamp;
        return rewardRate * remaining;
    }

    /// @notice ERC-4626 totalAssets excluding the unvested reward reserve.
    ///         Share price therefore grows linearly with time, not in steps.
    function totalAssets() public view override returns (uint256) {
        uint256 bal = IERC20(asset()).balanceOf(address(this));
        uint256 pending = _undistributedNow();
        return bal > pending ? bal - pending : 0;
    }

    /// @notice Permissionless. Recognises any AERE arrivals since the last
    ///         sync as a fresh reward and extends the drip schedule.
    ///         Called automatically inside every deposit / withdraw; expose
    ///         as external so AereSink (or any monitor) can refresh in between.
    function sync() public {
        uint256 bal = IERC20(asset()).balanceOf(address(this));
        if (bal > lastObservedBalance) {
            uint256 arrivals = bal - lastObservedBalance;
            uint256 undistrib = _undistributedNow();
            // AUDIT FIX R6 (HIGH): only RESTART the drip period if the
            // arrivals are significant relative to the unvested reserve OR
            // there is no active period. Tiny arrivals just top up the rate
            // without extending periodFinish — defeats the 1-wei-per-block
            // grief that would otherwise stall stakers' yield indefinitely.
            uint256 threshold = undistrib * DRIP_RESTART_THRESHOLD_BPS / 10_000;
            if (undistrib == 0 || arrivals >= threshold) {
                // significant arrival or fresh schedule
                uint256 newReserve = undistrib + arrivals;
                rewardRate = newReserve / DRIP_DURATION;
                periodFinish = block.timestamp + DRIP_DURATION;
            } else {
                // tiny arrival — keep periodFinish, only bump rewardRate
                uint256 remaining = periodFinish - block.timestamp;
                uint256 newReserve = undistrib + arrivals;
                rewardRate = newReserve / remaining;
            }
            emit RewardArrived(arrivals, rewardRate, periodFinish);
        }
        lastObservedBalance = bal;
    }

    /* --------------- hook sync into deposits / withdraws ---------------- */

    function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
        sync(); // observe any pending reward arrivals BEFORE share math
        super._deposit(caller, receiver, assets, shares);
        // The assets we just pulled in MUST be excluded from "arrivals" on
        // the next sync, so refresh the cached observation.
        lastObservedBalance = IERC20(asset()).balanceOf(address(this));
    }

    function _withdraw(
        address caller, address receiver, address owner,
        uint256 assets, uint256 shares
    ) internal override {
        sync();
        super._withdraw(caller, receiver, owner, assets, shares);
        lastObservedBalance = IERC20(asset()).balanceOf(address(this));
    }

    /* --------------------------------- views --------------------------------- */

    function pricePerShare() external view returns (uint256) {
        return convertToAssets(1e18);
    }

    /// @notice Convenience view: amount of AERE currently locked in the
    ///         reward reserve, awaiting linear vesting.
    function undistributedRewards() external view returns (uint256) {
        return _undistributedNow();
    }

    /// @notice Atomic state snapshot for fuzz tests / off-chain consumers
    /// that need balance + totalAssets + undistributed evaluated at exactly
    /// the same block. Eliminates cross-call drift in view-based invariant
    /// checks. AUDIT FIX R6.
    function atomicState() external view returns (
        uint256 balance,
        uint256 totalAssets_,
        uint256 undistributed,
        uint256 totalSupply_,
        uint256 rewardRate_,
        uint256 periodFinish_,
        uint256 lastObservedBalance_
    ) {
        balance = IERC20(asset()).balanceOf(address(this));
        undistributed = _undistributedNow();
        totalAssets_ = balance > undistributed ? balance - undistributed : 0;
        totalSupply_ = totalSupply();
        rewardRate_ = rewardRate;
        periodFinish_ = periodFinish;
        lastObservedBalance_ = lastObservedBalance;
    }
}

library SafeERC20Like {
    error SafeERC20FailedOperation();

    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        (bool ok, bytes memory ret) = address(token).call(
            abi.encodeCall(token.transferFrom, (from, to, value))
        );
        if (!ok) revert SafeERC20FailedOperation();
        if (ret.length > 0 && !abi.decode(ret, (bool))) revert SafeERC20FailedOperation();
    }
}
