The Sandbox.sol contractComing soon

Reference of the Sandbox.sol contract: game registry, the reward vault split every 24 hours, Pons fee harvest, administration, events, errors and security.

Sandbox.sol is the on-chain half of Sandbox, on Robinhood Chain (chain id 4663). It combines two things: a registry of published games (hash, URI, title, creator) and a vault of ETH, fed by the creator fees of the $SANDBOX token, redistributed every epoch — that is, every 24 hours — to the builders whose games were played the most.

Not deployed yet

Coming soon The contract is written and tested (46 unit tests and 2 tests on a Robinhood Chain fork) but not deployed yet. Until then, the platform runs as a draft registry.

Overview

RoleWhoCan
Creatorthe address that published the game (or received a transfer)update, hide, transfer; receives tips and shares
Operatoran address set by the ownersettle a finished epoch
Ownertwo-step transferpublish fee, operator, treasury, pause, hide a game, bind the token, sweep after 90 days
Anyonepublish, tip, claim a share for a creator, harvest fees

Types

Game

creatoraddress
Current creator: receives tips and epoch shares.
contentHashbytes32
keccak256 hash of the canonicalised document. Never zero.
uristring
Where to read the document, 1 to 256 bytes.
titlestring
Title, 1 to 64 bytes.
publishedAtuint64
Publication timestamp.
updatedAtuint64
Timestamp of the last write.
versionuint32
1 on publish, +1 on every update.
hiddenbool
Hidden by the creator or the owner; refuses tips.

Epoch

amountuint256
ETH reserved for the epoch at settlement.
totalWeightuint256
Sum of the settled weights.
claimeduint256
ETH already paid out (or swept).
settledAtuint64
0 until the epoch is settled.
sweptAtuint64
Non-zero after sweepUnclaimed: closes claims.

Constants

NameValueRole
MAX_PUBLISH_FEE0.01 ethercap of setPublishFee
EPOCH_LENGTH1 dayslength of an epoch (24 hours), from genesis
SWEEP_DELAY90 daysdelay before sweepUnclaimed (internal)
MAX_SETTLE_ENTRIES200games at most per settle (internal)
genesisimmutable uint64deployment timestamp
ponsFactoryimmutablePons V2 LaunchFactory

Game registry

Sandbox.solSolidity
function publish(bytes32 contentHash, string calldata uri, string calldata title)
    external payable returns (uint256 gameId);

function update(uint256 gameId, bytes32 contentHash, string calldata uri, string calldata title) external;

function setHidden(uint256 gameId, bool hidden) external;

function transferGame(uint256 gameId, address to) external;

function tip(uint256 gameId) external payable;

function gameCount() external view returns (uint256);
function getGame(uint256 gameId) external view returns (Game memory);
function gamesOf(address creator) external view returns (uint256[] memory);
FunctionWhoEffect
publishanyonecreates a game (ids from 1), requires msg.value ≥ publishFee (kept in the vault), emits Published
updatecreatorrewrites hash, URI and title, version + 1, emits Updated
setHiddencreator or ownerhides or unhides, emits Hidden
transferGamecreatorhands the game to another address; gamesOf lists follow; so do future shares
tipanyonesends 100 % of the amount to the creator; refuses 0 and hidden games

Site side, see Publishing a game.

Epochs and rewards

Sandbox.solSolidity
function currentEpoch() public view returns (uint256);
function unallocated() public view returns (uint256);

function settle(uint256 epoch, uint256[] calldata gameIds, uint256[] calldata weights, uint256 amount) external;

function claim(uint256 epoch, uint256 gameId) external;
function claimMany(uint256 epoch, uint256[] calldata gameIds) external;
function pending(uint256 epoch, uint256 gameId) external view returns (uint256);

function sweepUnclaimed(uint256 epoch) external;

// public state
mapping(uint256 => Epoch) public epochs;
mapping(uint256 => mapping(uint256 => uint256)) public weightOf;
mapping(uint256 => mapping(uint256 => bool)) public claimed;
uint256 public reserved;

settle

Operator only, once per 24-hour window. Writes the weights of a finished epoch (epoch < currentEpoch()), once, with 1 to 200 existing games, no duplicates, non-zero weights, and amount ≤ unallocated() (the vault’s balance minus ETH already reserved). The amount is added to reserved.

claim / claimMany / pending

Callable by anyone. The share amount × weight / totalWeight is paid to the game’s current creator, once per game per epoch. claimMany skips games already claimed or with no weight. pending returns the share still owed, 0 otherwise.

sweepUnclaimed

Owner only, 90 days after settlement: sends the epoch’s unclaimed remainder to the treasury and closes its claims (sweptAt).

Weight formula and schedule: Epoch rewards.

Pons fee harvest

Sandbox.solSolidity
function setPonsToken(address token) external;
function harvest() external;
receive() external payable;
  • setPonsToken(token): owner, once. The token must exist on the Pons factory and have this contract as creatorFeeRecipient, otherwise NotAPonsToken or WrongFeeRecipient.
  • harvest(): open to all, never blocked by the pause. While on the bonding curve it calls sweepFees on the curve; once the token is in its Uniswap V4 pool, sweepPoolFees on the hook. Each call is wrapped in try/catch, then the fee escrow is claimed if it holds anything. Emits Harvested(amount).
  • receive(): accepts ETH from anyone (pushed fees, donations) and emits Funded.

Administration

Sandbox.solSolidity
function setOperator(address operator_) external;
function setTreasury(address treasury_) external;
function setPublishFee(uint256 fee) external;
function pause() external;
function unpause() external;
function transferOwnership(address to) external;
function acceptOwnership() external;

All owner only, except acceptOwnership which must be called by the pendingOwner. Zero addresses are refused for the operator and treasury; setPublishFee refuses more than 0.01 ETH.

Events

EventEmitted by
Published(gameId, creator, contentHash, uri, title)publish
Updated(gameId, version, contentHash, uri, title)update
Hidden(gameId, hidden)setHidden
Transferred(gameId, from, to)transferGame
Tipped(gameId, from, amount)tip
Funded(from, amount)receive
Settled(epoch, amount, totalWeight, n)settle
Claimed(epoch, gameId, creator, share)claim, claimMany
Swept(epoch, amount)sweepUnclaimed
Harvested(amount)harvest
PonsTokenSet(token)setPonsToken
OperatorSet(operator)setOperator
TreasurySet(treasury)setTreasury
PublishFeeSet(fee)setPublishFee
PausedSet(paused)pause, unpause
OwnershipTransferStarted(from, to)transferOwnership
OwnershipTransferred(from, to)acceptOwnership

Errors

ErrorCause
NotOwner / NotPendingOwner / NotOperatorwrong caller for a role-gated function
NotCreator / NotAuthorisedonly the creator (or the owner for setHidden) may act
ZeroAddress / ZeroHash / ZeroAmountzero address, hash or amount
Haltedthe contract is paused
Reentrancyreentrant call blocked
NoSuchGame / GameHiddenunknown id; tip to a hidden game
BadTitle / BadUrititle outside 1..64 bytes; URI outside 1..256 bytes
FeeTooHigh / FeeNotPaidfee above the cap; msg.value too low
EpochNotOver / EpochSettled / EpochNotSettled / EpochSweptepoch state incompatible with the call
LengthMismatch / BadLength / ZeroWeight / DuplicateGame / Overallocatedinvalid settle arguments
AlreadyClaimed / NothingToClaimshare already paid; game has no weight in the epoch
TooEarly / NothingToSweepunder 90 days; nothing to sweep
PonsTokenAlreadySet / NoPonsToken / NotAPonsToken / WrongFeeRecipientPons token binding
SendFailedthe ETH transfer to the recipient failed

Security

  • No owner withdrawal. ETH only leaves through claim / claimMany, tip and sweepUnclaimed (to the treasury, after 90 days).
  • Reserved accounting. reserved tracks ETH promised to settled epochs; settle can only commit unallocated(), that is, the free part of the vault.
  • Two-step ownership (transferOwnership then acceptOwnership).
  • Reentrancy guard on tip, claim, claimMany, sweepUnclaimed and harvest.
  • Scoped pause. Pausing blocks publish, update, tip, settle, claim and claimMany; never receive or harvest.
  • Safe sweep. After sweepUnclaimed, sweptAt closes the epoch’s claims, so the same share is never paid twice.

Tests

Foundry, solc 0.8.26, EVM cancun, via_ir. 46 unit tests with Pons mocks (publishing, updates, hiding, transfer, tips, fee cap, settlement, exact shares and rounding, reentrancy, sweep, pause, ownership, every harvest branch) and 2 tests on a Robinhood Chain fork that launch a real token on Pons V2 with this contract as fee recipient, trade on the curve, harvest, settle and claim.

Shell
cd contracts
forge test -vv
forge test --match-path test/SandboxFork.t.sol -vv
forge test --gas-report

Gas costs

CallGas
publish()~240k (two strings into storage; ~200k with short ones)
update()31k–56k
setHidden()~33k
transferGame()80k median
tip()~34k
settle()143k median for 2 games; ~4.7M for 200 games
claim()99k median
claimMany()85k median
harvest()~110k on the fork
sweepUnclaimed()~99k

See also