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
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