Staking

0xb078cc304a0b264c5f3680dc0488954accd02e87

Verification
Verified
v0.8.30+commit.73712a01
Type
Contract
4,821 bytes
ABI entries
30
11 read · 6 write
License
none

Contract information

Address
0xb078cc304a0b264c5f3680dc0488954accd02e87
Chain
Robinhood Chain (4663)
Compiler
v0.8.30+commit.73712a01
Optimization
Enabled
Creation tx
0x498f2123cd…1f93a534e9

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (11)

distributor()address
enabled()bool
epoch()uint64, uint64, uint64, uint256
genesisBond()address
net()address
oracle()address
sNet()address
totalStaked()uint256
warmup(address)uint256, uint64
warmupEpochs()uint256
wired()bool

Events (3)

RebasedStakedUnstaked

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "net_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "sNet_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "warmupEpochs_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyEnabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyWired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotDeployer",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotEnabled",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGenesisBond",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotWired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ThirdPartyWarmup",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "epoch",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "distributed",
        "type": "uint256"
      }
    ],
    "name": "Rebased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Staked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Unstaked",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "claim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributor",
    "outputs": [
      {
        "internalType": "contract IDistributor",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "enable",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "enabled",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "epoch",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      },
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "genesisBond",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "net",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "oracle",
    "outputs": [
      {
        "internalType": "contract IPairOracle",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rebase",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sNet",
    "outputs": [
      {
        "internalType": "contract IsNET",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "stake",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalStaked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "unstake",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "warmup",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "gons",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "releaseEpoch",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "warmupEpochs",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "distributor_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "oracle_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "genesisBond_",
        "type": "address"
      }
    ],
    "name": "wire",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "wired",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.24;

import {IStaking} from "./interfaces/IStaking.sol";
import {IsNET} from "./interfaces/IsNET.sol";
import {IDistributor} from "./interfaces/IDistributor.sol";
import {IPairOracle} from "./interfaces/IPairOracle.sol";
import {IERC20} from "./interfaces/external/IERC20.sol";
import {Constants} from "./Constants.sol";
import {Wired} from "./abstract/Wired.sol";

/// @title Staking — NET ⇄ sNET (shareholder dividend program)
/// @notice 8h epochs, permissionless `rebase()` (specs/mechanism.md §2–3):
///         distributes the previous epoch's reward via sNET rebase, then
///         checkpoints the TWAP oracle and pulls the next epoch's
///         Distributor mint. One epoch advances per call; back-to-back calls
///         catch up if epochs were missed. Enabled by GenesisBond.finalize().
contract Staking is IStaking, Wired {
    error NotEnabled();
    error AlreadyEnabled();
    error NotGenesisBond();
    error TransferFailed();
    error ThirdPartyWarmup();

    IERC20 public immutable net;
    IsNET public immutable sNet;
    /// @notice Warmup in epochs (DEFAULT 0 — TUNE BEFORE DEPLOY). Constructor
    ///         arg (from Constants in deploy scripts) for testability.
    uint256 public immutable warmupEpochs;

    IDistributor public distributor;
    IPairOracle public oracle;
    address public genesisBond;

    bool public enabled;

    struct EpochState {
        uint64 length;
        uint64 number;
        uint64 end;
        uint256 distribute;
    }

    EpochState private _epoch;

    struct WarmupEntry {
        uint256 gons;
        uint64 releaseEpoch;
    }

    mapping(address => WarmupEntry) public warmup;

    constructor(address net_, address sNet_, uint256 warmupEpochs_) {
        net = IERC20(_nonZero(net_));
        sNet = IsNET(_nonZero(sNet_));
        warmupEpochs = warmupEpochs_;
    }

    /// @notice One-time wiring (Distributor ↔ Staking are mutually
    ///         referential). Frozen afterwards; on the emissions path this is
    ///         the only address set, and it cannot be re-set (invariant §6.7).
    function wire(address distributor_, address oracle_, address genesisBond_) external wiring {
        distributor = IDistributor(_nonZero(distributor_));
        oracle = IPairOracle(_nonZero(oracle_));
        genesisBond = _nonZero(genesisBond_);
    }

    /// @inheritdoc IStaking
    function enable() external {
        _checkWired();
        if (msg.sender != genesisBond) revert NotGenesisBond();
        if (enabled) revert AlreadyEnabled();
        enabled = true;
        _epoch = EpochState({
            length: uint64(Constants.EPOCH_LENGTH),
            number: 1,
            end: uint64(block.timestamp + Constants.EPOCH_LENGTH),
            distribute: 0
        });
    }

    /// @inheritdoc IStaking
    function epoch() external view returns (uint64, uint64, uint64, uint256) {
        return (_epoch.length, _epoch.number, _epoch.end, _epoch.distribute);
    }

    /// @inheritdoc IStaking
    function stake(address to, uint256 amount) external returns (uint256) {
        if (!enabled) revert NotEnabled();
        _rebaseIfDue();
        if (!net.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
        if (warmupEpochs == 0) {
            _sendSNet(to, amount);
        } else {
            // Third-party stakes may not touch someone else's warmup clock:
            // a dust stake would otherwise re-arm `releaseEpoch` forever.
            if (to != msg.sender) revert ThirdPartyWarmup();
            WarmupEntry storage entry = warmup[to];
            entry.gons += sNet.gonsForBalance(amount);
            entry.releaseEpoch = uint64(_epoch.number + warmupEpochs);
        }
        emit Staked(msg.sender, to, amount);
        return amount;
    }

    /// @notice Claims sNET matured past warmup (no-op path when warmup is 0).
    function claim(address to) external returns (uint256) {
        if (!enabled) revert NotEnabled();
        _rebaseIfDue();
        WarmupEntry memory entry = warmup[to];
        if (entry.gons == 0 || _epoch.number < entry.releaseEpoch) return 0;
        delete warmup[to];
        uint256 amount = sNet.balanceForGons(entry.gons);
        _sendSNet(to, amount);
        return amount;
    }

    /// @inheritdoc IStaking
    function unstake(address to, uint256 amount) external returns (uint256) {
        if (!enabled) revert NotEnabled();
        _rebaseIfDue();
        if (!sNet.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
        if (!net.transfer(to, amount)) revert TransferFailed();
        emit Unstaked(msg.sender, to, amount);
        return amount;
    }

    /// @inheritdoc IStaking
    function rebase() public {
        if (!enabled) revert NotEnabled();
        _rebaseIfDue();
    }

    function _rebaseIfDue() internal {
        if (block.timestamp < _epoch.end) return;
        // Distribute only when someone is staked; otherwise the queued NET
        // stays queued (never stranded) until stakers return.
        uint256 circulating = sNet.totalSupply() - sNet.balanceOf(address(this));
        uint256 distributed = 0;
        if (circulating > 0) {
            distributed = _epoch.distribute;
            sNet.rebase(distributed, _epoch.number);
            _epoch.distribute = 0;
        }
        emit Rebased(_epoch.number, distributed);
        _epoch.end += _epoch.length;
        _epoch.number += 1;
        // Keep the oracle live even if all keepers stall (D1).
        oracle.checkpoint();
        _epoch.distribute += distributor.distribute();
    }

    /// @inheritdoc IStaking
    function totalStaked() external view returns (uint256) {
        return sNet.totalSupply() - sNet.balanceOf(address(this));
    }

    function _sendSNet(address to, uint256 amount) internal {
        if (!sNet.transfer(to, amount)) revert TransferFailed();
    }
}
Chain explorer3015msChain node80ms