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
Overview
| Role | Who | Can |
|---|---|---|
| Creator | the address that published the game (or received a transfer) | update, hide, transfer; receives tips and shares |
| Operator | an address set by the owner | settle a finished epoch |
| Owner | two-step transfer | publish fee, operator, treasury, pause, hide a game, bind the token, sweep after 90 days |
| Anyone | publish, tip, claim a share for a creator, harvest fees |
Types
Game
creatoraddresscontentHashbytes32uristringtitlestringpublishedAtuint64updatedAtuint64versionuint32hiddenboolEpoch
amountuint256totalWeightuint256claimeduint256settledAtuint64sweptAtuint64Constants
| Name | Value | Role |
|---|---|---|
MAX_PUBLISH_FEE | 0.01 ether | cap of setPublishFee |
EPOCH_LENGTH | 1 days | length of an epoch (24 hours), from genesis |
SWEEP_DELAY | 90 days | delay before sweepUnclaimed (internal) |
MAX_SETTLE_ENTRIES | 200 | games at most per settle (internal) |
genesis | immutable uint64 | deployment timestamp |
ponsFactory | immutable | Pons V2 LaunchFactory |
Game registry
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);| Function | Who | Effect |
|---|---|---|
publish | anyone | creates a game (ids from 1), requires msg.value ≥ publishFee (kept in the vault), emits Published |
update | creator | rewrites hash, URI and title, version + 1, emits Updated |
setHidden | creator or owner | hides or unhides, emits Hidden |
transferGame | creator | hands the game to another address; gamesOf lists follow; so do future shares |
tip | anyone | sends 100 % of the amount to the creator; refuses 0 and hidden games |
Site side, see Publishing a game.
Epochs and rewards
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
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 ascreatorFeeRecipient, otherwiseNotAPonsTokenorWrongFeeRecipient.harvest(): open to all, never blocked by the pause. While on the bonding curve it callssweepFeeson the curve; once the token is in its Uniswap V4 pool,sweepPoolFeeson the hook. Each call is wrapped in try/catch, then the fee escrow is claimed if it holds anything. EmitsHarvested(amount).receive(): accepts ETH from anyone (pushed fees, donations) and emitsFunded.
Administration
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
| Event | Emitted 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
| Error | Cause |
|---|---|
NotOwner / NotPendingOwner / NotOperator | wrong caller for a role-gated function |
NotCreator / NotAuthorised | only the creator (or the owner for setHidden) may act |
ZeroAddress / ZeroHash / ZeroAmount | zero address, hash or amount |
Halted | the contract is paused |
Reentrancy | reentrant call blocked |
NoSuchGame / GameHidden | unknown id; tip to a hidden game |
BadTitle / BadUri | title outside 1..64 bytes; URI outside 1..256 bytes |
FeeTooHigh / FeeNotPaid | fee above the cap; msg.value too low |
EpochNotOver / EpochSettled / EpochNotSettled / EpochSwept | epoch state incompatible with the call |
LengthMismatch / BadLength / ZeroWeight / DuplicateGame / Overallocated | invalid settle arguments |
AlreadyClaimed / NothingToClaim | share already paid; game has no weight in the epoch |
TooEarly / NothingToSweep | under 90 days; nothing to sweep |
PonsTokenAlreadySet / NoPonsToken / NotAPonsToken / WrongFeeRecipient | Pons token binding |
SendFailed | the ETH transfer to the recipient failed |
Security
- No owner withdrawal. ETH only leaves through
claim/claimMany,tipandsweepUnclaimed(to the treasury, after 90 days). - Reserved accounting.
reservedtracks ETH promised to settled epochs;settlecan only commitunallocated(), that is, the free part of the vault. - Two-step ownership (
transferOwnershipthenacceptOwnership). - Reentrancy guard on tip, claim, claimMany, sweepUnclaimed and harvest.
- Scoped pause. Pausing blocks publish, update, tip, settle, claim and claimMany; never
receiveorharvest. - Safe sweep. After
sweepUnclaimed,sweptAtcloses 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.
cd contracts
forge test -vv
forge test --match-path test/SandboxFork.t.sol -vv
forge test --gas-reportGas costs
| Call | Gas |
|---|---|
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 |