Scribblehood
Contracts

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.

your walletapprove, thenbuyBasket(lines, total, deadline)totalIn USDGScribbleBasketfee = floor(total * feeBps / 10000)sum(lines.amountIn) must equaltotal minus fee, or it revertsholds nothing between callstreasurythe feeexactInputSingle, per lineSwapRouter02Uniswap V3recipient = youpoolsUSDG / tokenStock Tokens go straight back to the wallet, at least minOut each
One buyBasket call: the contract takes the fee, forwards each line to SwapRouter02 with you as the recipient, and the tokens never pass through it.

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.

ContractEnv varAddress on Robinhood Chain
ScribbleBasketNEXT_PUBLIC_SCRIBBLE_BASKET0x4624b0c0001BA9820476F570886D1378C575Dc04
ScribbleObjectsNEXT_PUBLIC_SCRIBBLE_OBJECTS0xDB07F19A2f7DA141295D78C4B75aa20734221C60
ScribbleRewardsNEXT_PUBLIC_SCRIBBLE_REWARDS0xdbF5f021683EC8a0fF05d3f7b22938a714eCbA99
SCRIBBLE tokenNEXT_PUBLIC_SCRIBBLE_TOKENset 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.

FunctionWhoWhat
buyBasket(Line[] lines, uint256 totalIn, uint256 deadline)anyonePulls 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)viewThe fee, floor(totalIn times feeBps / 10000), and the spendable remainder.
setFee(uint256 feeBps)ownerAt most 200 (2 percent). Starts at 50.
setTreasury(address)ownerWhere fees go. Never zero.
rescue(address token)ownerSends 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.

FunctionWhoWhat
mint(bytes32 summonId, string prompt, address primaryToken)anyone, payableMints 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)viewsummonId, primaryToken, minter, mintedAt. Reverts for ids that do not exist.
totalMinted()viewHow many exist.
setMintPrice, setBaseURI, setTreasuryownerThe price starts at zero.
withdraw()anyoneSends 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.

FunctionWhoWhat
setToken(address)owner, onceFor deployments made before SCRIBBLE existed. Cannot be changed after.
publishRound(uint256 round, bytes32 root, uint256 total)ownerOpens a round. Reverts unless the contract already holds enough tokens for every open round plus this one.
claim(uint256 round, uint256 amount, bytes32[] proof)anyoneOnce per round per account, while the round is open. claimMany does several rounds in one call.
closesAt(uint256 round)viewpublishedAt plus 90 days.
sweepExpired(uint256 round)anyoneAfter the window: sends the unclaimed remainder to the treasury and closes the round.
withdrawExcess()ownerAnything 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.