Contracts
Three small contracts on Robinhood Chain: a basket buyer, an object collection, and a merkle distributor for the weekly drop. What each does, who may call what, and where they are.
Addresses
Every address is read from an environment variable and is optional. When one is unset the feature that needs it hides or falls back, and the table below says so.
| Contract | Env var | Address on Robinhood Chain |
|---|---|---|
| ScribbleBasket | NEXT_PUBLIC_SCRIBBLE_BASKET | 0x4624b0c0001BA9820476F570886D1378C575Dc04 |
| ScribbleObjects | NEXT_PUBLIC_SCRIBBLE_OBJECTS | 0xDB07F19A2f7DA141295D78C4B75aa20734221C60 |
| ScribbleRewards | NEXT_PUBLIC_SCRIBBLE_REWARDS | 0xdbF5f021683EC8a0fF05d3f7b22938a714eCbA99 |
| SCRIBBLE token | NEXT_PUBLIC_SCRIBBLE_TOKEN | set after deployment |
All three are Solidity 0.8.24 on OpenZeppelin, compiled with the optimizer at 200 runs for the Cancun EVM, and verified on Blockscout by the deploy script. Source lives in contracts/src; tests in contracts/test.
ScribbleBasket
Buys a basket of Stock Tokens with USDG in one transaction through Uniswap V3 and keeps a small fee. It approves the router once, for the life of the contract, and the router only ever pulls what a swap needs.
| Function | Who | What |
|---|---|---|
buyBasket(Line[] lines, uint256 totalIn, uint256 deadline) | anyone | Pulls totalIn USDG, sends the fee to the treasury, swaps each line (token, fee tier, amountIn, minOut) to the caller. Reverts when the deadline passed, the basket is empty or over 12 lines, any amountIn is zero, the line amounts do not add up to totalIn minus the fee, or a swap returns less than its minOut. |
quoteFee(uint256 totalIn) | view | The fee, floor(totalIn times feeBps / 10000), and the spendable remainder. |
setFee(uint256 feeBps) | owner | At most 200 (2 percent). Starts at 50. |
setTreasury(address) | owner | Where fees go. Never zero. |
rescue(address token) | owner | Sends any token that ended up in the contract (a router refund, a mistaken transfer) to the treasury. |
ScribbleObjects
Objects as ERC-721s, collection "Scribblehood Objects", symbol SCRIB. A token records the summon id it came from, the Stock Token that leads its basket, who minted it and when. Metadata is baseURI + tokenId, which points at /api/nft/ on this site.
| Function | Who | What |
|---|---|---|
mint(bytes32 summonId, string prompt, address primaryToken) | anyone, payable | Mints the next id to the caller for exactly mintPrice in ETH. One per summon per wallet. The prompt is capped at 200 bytes and emitted in the Summoned event; primaryToken may be zero when the top line has no Stock Token. |
metaOf(uint256 tokenId) | view | summonId, primaryToken, minter, mintedAt. Reverts for ids that do not exist. |
totalMinted() | view | How many exist. |
setMintPrice, setBaseURI, setTreasury | owner | The price starts at zero. |
withdraw() | anyone | Sends collected mint fees to the treasury. The destination is fixed, so anyone may push the button. |
ScribbleRewards
A merkle distributor for the weekly Ink drops. The owner funds the contract with SCRIBBLE, publishes a round as a root and a total, and holders claim with a proof. Whatever is unclaimed after 90 days can be swept back to the treasury. Leaves are keccak256(bytes.concat(keccak256(abi.encode(round, account, amount)))) with sorted pairs, the OpenZeppelin shape, and src/lib/merkle.ts builds the same tree in TypeScript with a shared test vector.
| Function | Who | What |
|---|---|---|
setToken(address) | owner, once | For deployments made before SCRIBBLE existed. Cannot be changed after. |
publishRound(uint256 round, bytes32 root, uint256 total) | owner | Opens a round. Reverts unless the contract already holds enough tokens for every open round plus this one. |
claim(uint256 round, uint256 amount, bytes32[] proof) | anyone | Once per round per account, while the round is open. claimMany does several rounds in one call. |
closesAt(uint256 round) | view | publishedAt plus 90 days. |
sweepExpired(uint256 round) | anyone | After the window: sends the unclaimed remainder to the treasury and closes the round. |
withdrawExcess() | owner | Anything above what open rounds promise goes to the treasury. |
Ownership
All three use Ownable2Step: the owner proposes a new owner, and the new owner must call acceptOwnership() from its own wallet before anything changes. The deploy script makes the broadcasting wallet the owner of all three; scripts/transfer-ownership.sh 0xNewOwner starts the handover on each and prints the accept commands. The treasury is a separate address on each contract and defaults to the deployer.