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
- Creator
- 0xCfBd7e12A0…00068507B9
- 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