OreDistributor
0x7d80ebe8d128e9621ae2add02de6af4ef96c99a8
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
3,987 bytes
ABI entries
38
20 read · 7 write
License
none
Contract information
- Address
- 0x7d80ebe8d128e9621ae2add02de6af4ef96c99a8
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x03C6DAbB5e…09A6f8ee81
- Creation tx
- 0x0df2f85d2a…bf319d0cd9
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (20)
BASE_MULTIPLIER() → uint256
CYCLE_DURATION() → uint256
MAX_HOLD_CYCLES() → uint256
MULTIPLIER_STEP() → uint256
claimedBase(address) → uint256
clockStart(address) → uint256
currentCycle() → uint256
genesis() → uint256
getClaimAmount(address, uint256) → uint256, uint256, uint256
getHolderState(address) → uint256, uint256, uint256, bool, uint256
getInfo() → uint256, uint256, bytes32, uint256, uint256, uint256
getMultiplier(address) → uint256
goldToken() → address
lifetimePaid(address) → uint256
merkleRoot() → bytes32
owner() → address
rootCycle() → uint256
rootKeeper() → address
timeRemaining() → uint256
totalClaimed() → uint256
Events (6)
BonusEnabledClaimedGoldDepositedOwnershipTransferredRootKeeperSetRootUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "goldToken_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "cycle",
"type": "uint256"
}
],
"name": "BonusEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "baseGold",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "multiplier",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "payout",
"type": "uint256"
}
],
"name": "Claimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "GoldDeposited",
"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": "address",
"name": "keeper",
"type": "address"
}
],
"name": "RootKeeperSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bytes32",
"name": "root",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "uint256",
"name": "cycle",
"type": "uint256"
}
],
"name": "RootUpdated",
"type": "event"
},
{
"inputs": [],
"name": "BASE_MULTIPLIER",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CYCLE_DURATION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_HOLD_CYCLES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MULTIPLIER_STEP",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "cumulative",
"type": "uint256"
},
{
"internalType": "bytes32[]",
"name": "proof",
"type": "bytes32[]"
}
],
"name": "claim",
"outputs": [
{
"internalType": "uint256",
"name": "payout",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "claimedBase",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "clockStart",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "currentCycle",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "depositGold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "enableBonus",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "genesis",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"internalType": "uint256",
"name": "cumulative",
"type": "uint256"
}
],
"name": "getClaimAmount",
"outputs": [
{
"internalType": "uint256",
"name": "baseGold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "multiplier",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "payout",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "holder",
"type": "address"
}
],
"name": "getHolderState",
"outputs": [
{
"internalType": "uint256",
"name": "claimedBaseGold",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "multiplier",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "cyclesHeld",
"type": "uint256"
},
{
"internalType": "bool",
"name": "bonusEnabled",
"type": "bool"
},
{
"internalType": "uint256",
"name": "paidLifetime",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getInfo",
"outputs": [
{
"internalType": "uint256",
"name": "cycle",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "secondsLeft",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "root",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "rootAtCycle",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "goldReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lifetimeClaimed",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "holder",
"type": "address"
}
],
"name": "getMultiplier",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "goldToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lifetimePaid",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "merkleRoot",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rootCycle",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rootKeeper",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "root",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "cycle",
"type": "uint256"
}
],
"name": "setRoot",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "keeper",
"type": "address"
}
],
"name": "setRootKeeper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "timeRemaining",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalClaimed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title OreDistributor — snapshot gold rewards for all ORE holders
* @notice ORE holders earn PAXG automatically: every 15-minute cycle,
* an off-chain keeper snapshots holder balances, allocates the
* cycle's gold pro-rata, and posts a Merkle root of each
* holder's CUMULATIVE lifetime entitlement. Holders claim any
* time with a proof; unclaimed gold rolls forward forever.
*
* HOLD BONUS (opt-in): call enableBonus() once (via the site)
* to start a hold clock. Each full cycle without claiming adds
* +0.05x to your payout, capped at 1.30x after 6 cycles.
* Claiming resets the clock. Without opting in you always
* receive 1.00x — your full base entitlement.
*
* Trust model (stated plainly): the keeper computes balances
* off-chain. Snapshot data is published every cycle so anyone
* can recompute the root. The contract bounds damage: payouts
* are monotonic per address, and nothing can exceed deposits.
*/
contract OreDistributor is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
uint256 public constant CYCLE_DURATION = 15 minutes;
uint256 public constant BASE_MULTIPLIER = 100; // 1.00x
uint256 public constant MULTIPLIER_STEP = 5; // +0.05x / cycle
uint256 public constant MAX_HOLD_CYCLES = 6; // 1.30x cap
IERC20 public immutable goldToken; // PAXG on RHC
uint256 public immutable genesis; // cycle clock start
address public rootKeeper; // may post roots only
bytes32 public merkleRoot; // latest cumulative root
uint256 public rootCycle; // cycle the root covers
uint256 public totalClaimed; // lifetime PAXG paid
mapping(address => uint256) public claimedBase; // cumulative base paid
mapping(address => uint256) public clockStart; // 0 = bonus not enabled
mapping(address => uint256) public lifetimePaid; // incl. bonus, stats
event RootUpdated(bytes32 root, uint256 indexed cycle);
event BonusEnabled(address indexed holder, uint256 cycle);
event Claimed(address indexed holder, uint256 baseGold, uint256 multiplier, uint256 payout);
event GoldDeposited(uint256 amount);
event RootKeeperSet(address keeper);
constructor(address goldToken_) Ownable(msg.sender) {
goldToken = IERC20(goldToken_);
genesis = block.timestamp;
}
// ── time / cycles (pure clock — no poke needed) ──
function currentCycle() public view returns (uint256) {
return (block.timestamp - genesis) / CYCLE_DURATION + 1;
}
function timeRemaining() public view returns (uint256) {
uint256 elapsed = (block.timestamp - genesis) % CYCLE_DURATION;
return CYCLE_DURATION - elapsed;
}
// ── admin ──
function setRootKeeper(address keeper) external onlyOwner {
rootKeeper = keeper;
emit RootKeeperSet(keeper);
}
function depositGold(uint256 amount) external onlyOwner {
goldToken.safeTransferFrom(msg.sender, address(this), amount);
emit GoldDeposited(amount);
}
/// @notice Keeper posts the cumulative-entitlement root each cycle.
function setRoot(bytes32 root, uint256 cycle) external {
require(msg.sender == rootKeeper || msg.sender == owner(), "OreDist: not keeper");
require(cycle >= rootCycle, "OreDist: stale root");
merkleRoot = root;
rootCycle = cycle;
emit RootUpdated(root, cycle);
}
// ── hold bonus opt-in ──
function enableBonus() external {
require(clockStart[msg.sender] == 0, "OreDist: already enabled");
clockStart[msg.sender] = currentCycle();
emit BonusEnabled(msg.sender, currentCycle());
}
function getMultiplier(address holder) public view returns (uint256) {
uint256 start = clockStart[holder];
if (start == 0) return BASE_MULTIPLIER;
uint256 held = currentCycle() - start;
if (held > MAX_HOLD_CYCLES) held = MAX_HOLD_CYCLES;
return BASE_MULTIPLIER + held * MULTIPLIER_STEP;
}
// ── views for the dashboard ──
/// @param cumulative the holder's total lifetime entitlement per the
/// latest published snapshot (supplied by the interface).
function getClaimAmount(address holder, uint256 cumulative)
public view returns (uint256 baseGold, uint256 multiplier, uint256 payout)
{
uint256 already = claimedBase[holder];
baseGold = cumulative > already ? cumulative - already : 0;
multiplier = getMultiplier(holder);
payout = (baseGold * multiplier) / BASE_MULTIPLIER;
}
function getInfo() external view returns (
uint256 cycle, uint256 secondsLeft, bytes32 root,
uint256 rootAtCycle, uint256 goldReserve, uint256 lifetimeClaimed
) {
return (currentCycle(), timeRemaining(), merkleRoot, rootCycle,
goldToken.balanceOf(address(this)), totalClaimed);
}
function getHolderState(address holder) external view returns (
uint256 claimedBaseGold, uint256 multiplier,
uint256 cyclesHeld, bool bonusEnabled, uint256 paidLifetime
) {
uint256 start = clockStart[holder];
uint256 held = start == 0 ? 0 : currentCycle() - start;
if (held > MAX_HOLD_CYCLES) held = MAX_HOLD_CYCLES;
return (claimedBase[holder], getMultiplier(holder), held,
start != 0, lifetimePaid[holder]);
}
// ── claim ──
/// @param cumulative total lifetime entitlement from the snapshot
/// @param proof Merkle proof for leaf (holder, cumulative)
function claim(uint256 cumulative, bytes32[] calldata proof)
external nonReentrant returns (uint256 payout)
{
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, cumulative))));
require(MerkleProof.verify(proof, merkleRoot, leaf), "OreDist: bad proof");
uint256 already = claimedBase[msg.sender];
require(cumulative > already, "OreDist: nothing to claim");
uint256 baseGold = cumulative - already;
uint256 multiplier = getMultiplier(msg.sender);
payout = (baseGold * multiplier) / BASE_MULTIPLIER;
uint256 bal = goldToken.balanceOf(address(this));
require(bal >= baseGold, "OreDist: reserve low");
if (payout > bal) payout = bal; // bonus degrades gracefully, base always paid
claimedBase[msg.sender] = cumulative;
if (clockStart[msg.sender] != 0) clockStart[msg.sender] = currentCycle();
lifetimePaid[msg.sender] += payout;
totalClaimed += payout;
goldToken.safeTransfer(msg.sender, payout);
emit Claimed(msg.sender, baseGold, multiplier, payout);
}
}
Chain explorer1870msChain node79ms