GenesisRewardDistributor

0x49574834f07c186e340d9372f867355e04bdc488

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
2,240 bytes
ABI entries
29
6 read · 8 write
License
none

Contract information

Address
0x49574834f07c186e340d9372f867355e04bdc488
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x1d22fd88e0…7728845655

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (6)

genesis()address
lastDistributionTime()uint256
owner()address
pendingRewards()uint256
rewardPerSecond()uint256
tracker()address

Events (7)

ClockResetFundedOwnershipTransferredRewardPerSecondUpdatedRewardsDistributedTokenWithdrawnTrackerSet

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "genesis_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "NotTracker",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TrackerAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TrackerNotSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "ClockReset",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Funded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "rate",
        "type": "uint256"
      }
    ],
    "name": "RewardPerSecondUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RewardsDistributed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TokenWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "tracker",
        "type": "address"
      }
    ],
    "name": "TrackerSet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "distribute",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "fund",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "genesis",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lastDistributionTime",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "resetClock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardPerSecond",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "rate",
        "type": "uint256"
      }
    ],
    "name": "setRewardPerSecond",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tracker_",
        "type": "address"
      }
    ],
    "name": "setTracker",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tracker",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "withdrawToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IGenesisTracker {
    function updateGlobalRewards() external;
}

/// @notice Reward vault for the GENESIS staking farm (GMX RewardDistributor
/// pattern): this contract holds ONLY the reward pool, at its own address,
/// structurally separated from staked principal (which lives in the
/// GenesisStaking tracker). It streams `rewardPerSecond` GENESIS to the
/// tracker, which pulls via `distribute()` on every user interaction. The
/// stream is capped by this contract's balance and the tracker pauses it
/// (via `resetClock`) whenever nobody is staked, so rewards can never accrue
/// to nobody. Because user deposits never touch this address, the owner may
/// withdraw unstreamed rewards (`withdrawToken`) — the reward pool is
/// discretionary; principal in the tracker is not.
contract GenesisRewardDistributor is Ownable {
    using SafeERC20 for IERC20;

    error ZeroAddress();
    error TrackerAlreadySet();
    error TrackerNotSet();
    error NotTracker();

    event TrackerSet(address tracker);
    event RewardsDistributed(uint256 amount);
    event ClockReset(uint256 timestamp);
    event RewardPerSecondUpdated(uint256 rate);
    event Funded(address indexed from, uint256 amount);
    event TokenWithdrawn(address indexed token, address to, uint256 amount);

    /// @notice Token streamed as rewards (same token users stake).
    IERC20 public immutable genesis;
    /// @notice The GenesisStaking contract; the only address allowed to pull.
    address public tracker;
    /// @notice GENESIS wei streamed per second while stakers exist.
    uint256 public rewardPerSecond;
    uint256 public lastDistributionTime;

    constructor(address genesis_) Ownable(msg.sender) {
        if (genesis_ == address(0)) revert ZeroAddress();
        genesis = IERC20(genesis_);
        lastDistributionTime = block.timestamp;
    }

    /// @notice One-time wiring; the tracker address is fixed thereafter.
    function setTracker(address tracker_) external onlyOwner {
        if (tracker_ == address(0)) revert ZeroAddress();
        if (tracker != address(0)) revert TrackerAlreadySet();
        tracker = tracker_;
        emit TrackerSet(tracker_);
    }

    /// @notice Rewards accrued since the last distribution, capped by balance
    /// so the stream simply ends when the pool runs dry.
    function pendingRewards() public view returns (uint256) {
        uint256 amount = (block.timestamp - lastDistributionTime) * rewardPerSecond;
        uint256 balance = genesis.balanceOf(address(this));
        return amount > balance ? balance : amount;
    }

    /// @notice Pays accrued rewards to the tracker. Tracker-only: the tracker
    /// calls this on every interaction while stakers exist.
    function distribute() external returns (uint256 amount) {
        if (msg.sender != tracker) revert NotTracker();
        amount = pendingRewards();
        lastDistributionTime = block.timestamp;
        if (amount > 0) {
            genesis.safeTransfer(tracker, amount);
            emit RewardsDistributed(amount);
        }
    }

    /// @notice Skips the clock to now without paying. Tracker-only — called on
    /// the first stake after the pool empties out, so time spent with zero
    /// stakers never accrues rewards (the stream pauses instead).
    function resetClock() external {
        if (msg.sender != tracker) revert NotTracker();
        lastDistributionTime = block.timestamp;
        emit ClockReset(block.timestamp);
    }

    /// @notice Change the emission rate. Settles the tracker at the old rate
    /// first so the change is never retroactive.
    function setRewardPerSecond(uint256 rate) external onlyOwner {
        if (tracker == address(0)) revert TrackerNotSet();
        IGenesisTracker(tracker).updateGlobalRewards();
        rewardPerSecond = rate;
        emit RewardPerSecondUpdated(rate);
    }

    /// @notice Top up the reward pool. Settles the tracker BEFORE the tokens
    /// arrive: if the pool sat empty for a while, that gap settles against the
    /// old (zero) balance rather than being paid retroactively from the new
    /// funds. Always fund through here, never by raw transfer.
    function fund(uint256 amount) external onlyOwner {
        if (tracker != address(0)) IGenesisTracker(tracker).updateGlobalRewards();
        genesis.safeTransferFrom(msg.sender, address(this), amount);
        emit Funded(msg.sender, amount);
    }

    /// @notice Owner escape hatch — may withdraw any token, including GENESIS:
    /// this address only ever holds the discretionary reward pool, never user
    /// deposits (those live in the tracker, whose rescue refuses GENESIS).
    function withdrawToken(address token, address to, uint256 amount) external onlyOwner {
        if (to == address(0)) revert ZeroAddress();
        IERC20(token).safeTransfer(to, amount);
        emit TokenWithdrawn(token, to, amount);
    }
}
Chain explorer4796msChain node81ms