VoomBoard
0xbe8d75da8e17f32090e5615bbf65536f1b5561c6
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
7,400 bytes
ABI entries
30
16 read · 7 write
License
none
Contract information
- Address
- 0xbe8d75da8e17f32090e5615bbf65536f1b5561c6
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x0d608C77e1…01d82Eda0C
- Creation tx
- 0xf7cc69fc07…a49c361d72
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (16)
PROTOCOL_FEE_BPS() → uint256
ROUND_DURATION() → uint256
TILES() → uint8
claimed(uint256, address) → bool
currentRound() → uint256
feeTo() → address
feesAccrued() → uint256
myStakes(uint256, address) → uint256[25]
owner() → address
rewardBudget() → uint256
roundInfo(uint256) → uint256, uint256, uint256, bool, uint8, uint256, uint256, uint256
stake(uint256, uint8, address) → uint256
tileTotals(uint256) → uint256[25]
timeLeft() → uint256
voom() → address
voomPerRound() → uint256
Events (6)
ClaimedDeployFeesWithdrawnFinalizedRoundLockedRoundOpened
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_voom",
"type": "address"
},
{
"internalType": "address",
"name": "_feeTo",
"type": "address"
},
{
"internalType": "uint256",
"name": "_voomPerRound",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "round",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "miner",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "payout",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "voomMinted",
"type": "uint256"
}
],
"name": "Claimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "round",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "miner",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Deploy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeesWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "round",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint8",
"name": "elim",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "survivorPot",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "survivorStake",
"type": "uint256"
}
],
"name": "Finalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "round",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "settleBlock",
"type": "uint256"
}
],
"name": "RoundLocked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "round",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "start",
"type": "uint256"
}
],
"name": "RoundOpened",
"type": "event"
},
{
"inputs": [],
"name": "PROTOCOL_FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ROUND_DURATION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TILES",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "r",
"type": "uint256"
}
],
"name": "claim",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "claimed",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "currentRound",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8[]",
"name": "tiles",
"type": "uint8[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"name": "deploy",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "feeTo",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feesAccrued",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "r",
"type": "uint256"
}
],
"name": "finalize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "r",
"type": "uint256"
},
{
"internalType": "address",
"name": "who",
"type": "address"
}
],
"name": "myStakes",
"outputs": [
{
"internalType": "uint256[25]",
"name": "out",
"type": "uint256[25]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poke",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rewardBudget",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "r",
"type": "uint256"
}
],
"name": "roundInfo",
"outputs": [
{
"internalType": "uint256",
"name": "start",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "endsAt",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "settleBlock",
"type": "uint256"
},
{
"internalType": "bool",
"name": "finalized",
"type": "bool"
},
{
"internalType": "uint8",
"name": "elim",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "total",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "survivorPot",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "survivorStake",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_feeTo",
"type": "address"
}
],
"name": "setFeeTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "",
"type": "uint8"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "stake",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "r",
"type": "uint256"
}
],
"name": "tileTotals",
"outputs": [
{
"internalType": "uint256[25]",
"name": "",
"type": "uint256[25]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "timeLeft",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "o",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "voom",
"outputs": [
{
"internalType": "contract VoomToken",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "voomPerRound",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdrawFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {VoomToken} from "./VoomToken.sol";
/// @title VoomBoard
/// @notice The Voom elimination lottery. Each 60s round, miners deploy ETH onto a
/// 5x5 board of 25 tiles. At settlement one tile is drawn and ELIMINATED (burned):
/// its ETH is shared among the survivors (everyone on the other 24 tiles), who also
/// get their own stake back, less a 10% protocol fee. Survivors also mint $VOOM.
/// Burn. Survive. Take it all.
///
/// $VOOM rewards come out of a pre-funded budget held by this contract, never from a
/// mint: `VoomToken` has no mint function at all, so there is no privileged role here
/// that a scanner could flag and no way for anyone to inflate the supply. If the budget
/// empties, `claim` simply pays 0 $VOOM and the ETH side keeps working untouched.
///
/// Randomness: commit-reveal on a future block hash. When a round closes it commits
/// to `block.number + 1`; settlement draws from that block's hash, so no one can
/// grind the outcome. (Pyth Entropy is the intended production upgrade.)
contract VoomBoard {
uint8 public constant TILES = 25;
uint256 public constant ROUND_DURATION = 60;
uint256 public constant PROTOCOL_FEE_BPS = 1000; // 10%
VoomToken public immutable voom;
uint256 public immutable voomPerRound; // VOOM minted to survivors each round
address public owner;
address public feeTo;
uint256 public currentRound;
/// @notice Fees owed to `feeTo`, withdrawn with `withdrawFees`. Accrued rather than
/// pushed: a `feeTo` that reverts on receive would otherwise make `finalize`
/// revert forever, and since `claim` requires a finalized round that would
/// freeze every survivor's ETH permanently.
uint256 public feesAccrued;
struct Round {
uint64 start;
uint64 settleBlock; // 0 until the round is locked
bool finalized;
uint8 elim; // eliminated tile (valid once finalized)
uint256 total; // total ETH deployed this round
uint256 survivorPot; // ETH shared to survivors after fee (set at finalize)
uint256 survivorStake; // total - tileTotal[elim] (set at finalize)
uint256[25] tileTotal;
}
mapping(uint256 => Round) internal _rounds;
// round => tile => player => ETH staked
mapping(uint256 => mapping(uint8 => mapping(address => uint256))) public stake;
mapping(uint256 => mapping(address => bool)) public claimed;
uint256 private _lock = 1;
modifier nonReentrant() {
require(_lock == 1, "reentrant");
_lock = 2;
_;
_lock = 1;
}
event Deploy(uint256 indexed round, address indexed miner, uint256 amount);
event RoundLocked(uint256 indexed round, uint256 settleBlock);
event RoundOpened(uint256 indexed round, uint256 start);
event Finalized(uint256 indexed round, uint8 elim, uint256 survivorPot, uint256 survivorStake);
event Claimed(uint256 indexed round, address indexed miner, uint256 payout, uint256 voomMinted);
event FeesWithdrawn(address indexed to, uint256 amount);
constructor(address _voom, address _feeTo, uint256 _voomPerRound) {
require(_voom != address(0), "voom");
require(_feeTo != address(0), "feeTo");
owner = msg.sender;
feeTo = _feeTo;
voomPerRound = _voomPerRound;
voom = VoomToken(_voom);
currentRound = 1;
_rounds[1].start = uint64(block.timestamp);
emit RoundOpened(1, block.timestamp);
}
// ---- play ----
function deploy(uint8[] calldata tiles, uint256[] calldata amounts) external payable nonReentrant {
poke();
uint256 r = currentRound;
Round storage R = _rounds[r];
require(block.timestamp < uint256(R.start) + ROUND_DURATION, "round closed");
require(tiles.length == amounts.length && tiles.length > 0, "len");
uint256 sum;
for (uint256 i; i < tiles.length; i++) {
uint8 t = tiles[i];
uint256 a = amounts[i];
require(t < TILES, "tile");
require(a > 0, "amount");
stake[r][t][msg.sender] += a;
R.tileTotal[t] += a;
sum += a;
}
require(sum == msg.value, "value");
R.total += sum;
emit Deploy(r, msg.sender, sum);
}
/// @notice advance the clock: lock an expired round and open the next one. Permissionless.
function poke() public {
Round storage R = _rounds[currentRound];
if (R.settleBlock == 0 && block.timestamp >= uint256(R.start) + ROUND_DURATION) {
R.settleBlock = uint64(block.number + 1);
emit RoundLocked(currentRound, R.settleBlock);
currentRound += 1;
_rounds[currentRound].start = uint64(block.timestamp);
emit RoundOpened(currentRound, block.timestamp);
}
}
/// @notice draw the eliminated tile for a locked round once its commit block exists. Permissionless.
function finalize(uint256 r) public nonReentrant {
Round storage R = _rounds[r];
require(R.settleBlock != 0 && !R.finalized, "not finalizable");
require(block.number > R.settleBlock, "wait for block");
bytes32 bh = blockhash(R.settleBlock);
if (bh == bytes32(0)) {
// The commit block aged out (>256 blocks) and its hash is gone. Re-commit to a
// fresh future block instead of falling back to a hash the caller can already
// read — that fallback would let anyone grind the draw by simulating finalize
// each block and only broadcasting when the eliminated tile suits them.
R.settleBlock = uint64(block.number + 1);
emit RoundLocked(r, R.settleBlock);
return;
}
uint8 elim = uint8(uint256(keccak256(abi.encodePacked(bh, r))) % TILES);
R.elim = elim;
R.finalized = true;
uint256 pot = R.tileTotal[elim];
uint256 surv = R.total - pot;
R.survivorStake = surv;
if (pot > 0) {
if (surv == 0) {
// no survivors (all ETH sat on the burned tile) → house takes the pot
feesAccrued += pot;
} else {
uint256 fee = pot * PROTOCOL_FEE_BPS / 10000;
R.survivorPot = pot - fee;
feesAccrued += fee;
}
}
emit Finalized(r, elim, R.survivorPot, surv);
}
/// @notice survivors claim: their surviving stake back + a pro-rata share of the burned pot + $VOOM.
function claim(uint256 r) external nonReentrant {
Round storage R = _rounds[r];
require(R.finalized, "not finalized");
require(!claimed[r][msg.sender], "claimed");
claimed[r][msg.sender] = true;
uint8 elim = R.elim;
uint256 surviving;
for (uint8 t; t < TILES; t++) {
if (t == elim) continue;
surviving += stake[r][t][msg.sender];
}
require(surviving > 0, "nothing to claim"); // burned-only stakes have nothing left
uint256 share = R.survivorStake > 0 ? R.survivorPot * surviving / R.survivorStake : 0;
uint256 payout = surviving + share;
uint256 voomAmt = R.survivorStake > 0 ? voomPerRound * surviving / R.survivorStake : 0;
// Pay out of the pre-funded budget, capped by what is actually left. A dry budget
// must never block a survivor from getting their ETH back, so this is clamped
// rather than required.
if (voomAmt > 0) {
uint256 budget = voom.balanceOf(address(this));
if (voomAmt > budget) voomAmt = budget;
if (voomAmt > 0) voom.transfer(msg.sender, voomAmt);
}
_send(msg.sender, payout);
emit Claimed(r, msg.sender, payout, voomAmt);
}
/// @notice $VOOM left in the emission budget. Anyone can top it up with a plain transfer.
function rewardBudget() external view returns (uint256) {
return voom.balanceOf(address(this));
}
// ---- views ----
function tileTotals(uint256 r) external view returns (uint256[25] memory) {
return _rounds[r].tileTotal;
}
function myStakes(uint256 r, address who) external view returns (uint256[25] memory out) {
for (uint8 t; t < TILES; t++) {
out[t] = stake[r][t][who];
}
}
function roundInfo(uint256 r)
external
view
returns (uint256 start, uint256 endsAt, uint256 settleBlock, bool finalized, uint8 elim, uint256 total, uint256 survivorPot, uint256 survivorStake)
{
Round storage R = _rounds[r];
return (R.start, uint256(R.start) + ROUND_DURATION, R.settleBlock, R.finalized, R.elim, R.total, R.survivorPot, R.survivorStake);
}
function timeLeft() external view returns (uint256) {
uint256 endsAt = uint256(_rounds[currentRound].start) + ROUND_DURATION;
return block.timestamp >= endsAt ? 0 : endsAt - block.timestamp;
}
/// @notice Pull accrued protocol fees to `feeTo`. Permissionless — it can only ever
/// pay `feeTo`, so anyone may trigger it and a stuck fee sink cannot hold the
/// game hostage.
function withdrawFees() external nonReentrant {
uint256 amount = feesAccrued;
require(amount > 0, "no fees");
feesAccrued = 0;
_send(feeTo, amount);
emit FeesWithdrawn(feeTo, amount);
}
// ---- admin (fee sink only) ----
function setFeeTo(address _feeTo) external {
require(msg.sender == owner, "only owner");
feeTo = _feeTo;
}
function transferOwnership(address o) external {
require(msg.sender == owner, "only owner");
owner = o;
}
function _send(address to, uint256 amount) internal {
(bool ok,) = to.call{value: amount}("");
require(ok, "native send");
}
}
Chain explorer5371msChain node81ms