// SPDX-License-Identifier: UNLICENSED
// [NAME] — a hand-written solidity contract on robinhood chain.
// the state only rises, the record only lengthens, the rain only comes at a record.
//
// no inheritance from libraries, no proxy, no admin slot, no delegatecall.
// the abi is not the specification. the bytecode is.

pragma solidity 0.8.26;

interface IGasInfo {
    /// wei the chain has collected from users for posting their data,
    /// against wei the chain has actually paid ethereum to post it.
    function getL1RewardRate() external view returns (uint64);
    function getL1PricingSurplus() external view returns (int256);
    function getLastL1PricingUpdateTime() external view returns (uint64);
    function getL1PricingUnitsSinceUpdate() external view returns (uint64);
}

interface IERC20Minimal {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract Clud {
    // ---------------------------------------------------------------- state

    /// the worst shortfall ever observed, in wei. only rises.
    uint256 public level;
    /// wei held for the next rain.
    uint256 public vault;
    /// how many times the level has been beaten.
    uint64 public rains;
    /// how many times observe() has run.
    uint64 public observations;
    /// the block number of the previous observation.
    uint64 public lastObservedBlock;
    /// blocks that passed out of blockhash reach with no observer. only rises.
    uint64 public unseen;

    // staking, fixed point, constant time claim
    uint256 public totalStaked;
    uint256 public rewardIndex; // scaled by 1e27
    mapping(address => uint256) public staked;
    mapping(address => uint256) public rewardDebt;

    IGasInfo private constant GAS_INFO = IGasInfo(0x000000000000000000000000000000000000006C);
    IERC20Minimal private immutable TOKEN;

    /// the caller's cut of the vault on a rain, in basis points.
    uint256 private constant OBSERVER_BPS = 200; // 2%
    uint256 private constant BPS = 10_000;
    uint256 private constant INDEX_SCALE = 1e27;
    /// the evm remembers this many block hashes and no more.
    uint64 private constant HORIZON = 256;

    event Observed(
        uint64 indexed observation,
        uint256 shortfall,
        uint256 level,
        uint64 interval,
        uint64 unseenAdded
    );
    event Rained(uint64 indexed rain, uint256 shortfall, uint256 paidCaller, uint256 sweptPool);

    constructor(address token) payable {
        TOKEN = IERC20Minimal(token);
        lastObservedBlock = uint64(block.number);
        // no owner is written. there is no storage slot for one.
    }

    // -------------------------------------------------------------- observe

    /// permissionless. takes no arguments. reverts on nothing.
    function observe() external {
        uint64 nowBlock = uint64(block.number);
        uint64 last = lastObservedBlock;

        // continuity. if the previous observation is out of blockhash reach
        // the interval cannot be proven, and the gap is recorded as unproven.
        uint64 unseenAdded = 0;
        if (nowBlock > last + HORIZON || blockhash(last) == bytes32(0)) {
            unseenAdded = nowBlock - last - (nowBlock > HORIZON ? HORIZON : nowBlock);
            unseen += unseenAdded;
        }

        // the scalar. what the chain collected for data against what it paid.
        int256 surplus = GAS_INFO.getL1PricingSurplus();
        uint256 shortfall = surplus < 0 ? uint256(-surplus) : 0;

        observations += 1;
        lastObservedBlock = nowBlock;

        emit Observed(observations, shortfall, level, nowBlock - last, unseenAdded);

        if (shortfall <= level) {
            // the caller has paid gas and receives nothing.
            return;
        }

        level = shortfall;
        rains += 1;

        uint256 pot = vault;
        vault = 0;
        uint256 toCaller = (pot * OBSERVER_BPS) / BPS;
        uint256 toPool = pot - toCaller;

        if (totalStaked > 0) {
            rewardIndex += (toPool * INDEX_SCALE) / totalStaked;
        } else {
            vault += toPool;
            toPool = 0;
        }

        emit Rained(rains, shortfall, toCaller, toPool);
        (bool ok, ) = msg.sender.call{value: toCaller}("");
        require(ok);
    }

    // ---------------------------------------------------------------- pool

    function stake(uint256 amount) external {
        _settle(msg.sender);
        staked[msg.sender] += amount;
        totalStaked += amount;
        require(TOKEN.transferFrom(msg.sender, address(this), amount));
    }

    /// no lockup, no exit penalty.
    function unstake(uint256 amount) external {
        _settle(msg.sender);
        staked[msg.sender] -= amount;
        totalStaked -= amount;
        require(TOKEN.transfer(msg.sender, amount));
    }

    /// constant time regardless of how many dry observations were sat through.
    function claim() external {
        uint256 owed = _settle(msg.sender);
        if (owed == 0) return;
        (bool ok, ) = msg.sender.call{value: owed}("");
        require(ok);
    }

    mapping(address => uint256) private accrued;

    function _settle(address who) private returns (uint256) {
        uint256 idx = rewardIndex;
        uint256 delta = idx - rewardDebt[who];
        if (delta > 0 && staked[who] > 0) {
            accrued[who] += (staked[who] * delta) / INDEX_SCALE;
        }
        rewardDebt[who] = idx;
        uint256 owed = accrued[who];
        accrued[who] = 0;
        return owed;
    }

    function pending(address who) external view returns (uint256) {
        uint256 delta = rewardIndex - rewardDebt[who];
        return accrued[who] + (staked[who] * delta) / INDEX_SCALE;
    }

    // ---------------------------------------------------------------- vault

    /// anything sent here fills the vault and can leave only through a rain.
    receive() external payable {
        vault += msg.value;
    }

    // there is no setter, no reset, no pause, no migration, no withdrawal,
    // no upgrade path and no admin. this is the whole of the contract.
}