Docs menu

SNAG docs, for developers

Contract

SnagCollection is an ERC-721 with Ownable2Step, Pausable and EIP712("Snag Collection","1"). Every function is non-payable: the contract never holds or moves value, has no withdraw, no fee, no pot and no payout.

Status

Minting functions

mintForBuy, callable by anyone, normally the keeper:

function mintForBuy(
    address buyer,
    bytes32 buyRef,
    uint256 amountIn,
    uint256 deadline,
    bytes calldata signature
) external whenNotPaused

Requires the contract unpaused, block.timestamp <= deadline, buyRef unused, amountIn >= minBuy, a non-zero buyer, and a valid EIP-712 signature by signer over the authorisation. It marks the buy reference used before minting, so no path can reach the mint twice for one buy.

mintFromHook, reserved for a future Uniswap V4 afterSwap hook:

function mintFromHook(
    address buyer,
    uint256 amountIn,
    bytes32 buyRef
) external whenNotPaused

Only callable by the address in hook, which is zero by default. Same replay guard and minimum as mintForBuy, no signature needed, because the hook itself is the trusted caller.

Read functions

FunctionReturns
traitsOf(tokenId)The four option indexes (character, headwear, glass, frame) for a minted piece.
traitNamesOf(tokenId)The same four traits, as names.
traitKeys()The four group keys, in table order.
seedFor(pool, buyer, swapIndex, amountIn)Pure form of the seed rule, for previews.
traitsForSeed(seed)Pure form of the trait rule, for previews and parity tests.
authorizationHash(buyer, buyRef, amountIn, deadline)The exact digest the signer signs, so an authorisation can be checked offline.
imageForTraits(character, headwear, glass, frame)Pure: the tile's SVG string for any four trait indexes, minted or not. What docs/js/docs-tile.js mirrors.
imageSVG(tokenId)The piece's artwork as an SVG string, built entirely on chain, for a minted piece.
tokenURI(tokenId)The full metadata, base64-encoded JSON with the base64-encoded SVG inside it.
settingsFrozen()True once token, pool and minBuy can no longer change.

Admin functions

setSigner, setHook (owner only, any time), and setToken, setPool, setMinBuy (owner only, until the first mint, then frozen forever). pause and unpause. renounceOwnership is overridden to always revert, so a leaked signer key can always be rotated by someone. Ownership itself moves in two steps.

Events

event SignerSet(address indexed signer);
event HookSet(address indexed hook);
event TokenSet(address indexed token);
event PoolSet(address indexed pool);
event MinBuySet(uint256 minBuy);
event Snagged(uint256 indexed tokenId, address indexed buyer, bytes32 indexed buyRef, bytes32 seed);

Custom errors

error BadSignature();
error AuthorizationExpired();
error BuyAlreadyUsed();
error BelowMinBuy();
error ZeroBuyer();
error NotHook();
error SettingsFrozen();
error SupplyExhausted();
error RenounceDisabled();
error ZeroAddress();

The EIP-712 domain and type string

Domain name "Snag Collection", version "1". The domain separator also binds the chain id and this contract's address, so a signature made for another chain or another collection can never be replayed here. The type the signer signs:

MintAuthorization(address buyer,bytes32 buyRef,uint256 amountIn,uint256 deadline)

This exact string is byte-identical in the contract, in api/_snag-mint-lib.js and in both test files. checks/snag-mint.cjs fails the build if any copy drifts.

The seed formula

A piece's four traits come from one seed, computed and stored once, at mint time:

bytes32 poolId = bytes32(uint256(uint160(pool)));
bytes32 seed = keccak256(abi.encodePacked(poolId, buyer, swapCount, amountIn));

That is abi.encodePacked, not abi.encode: the buyer is packed as 20 bytes, matching the site's own js/snag-market.js exactly, so what a visitor saw in the lab before a real buy is what they get after one. Reading two hex characters of that seed per trait group, in the order character, headwear, glass, frame, taken modulo 1000 and walked against the cumulative weights, picks the four options. See Rarity for the weight tables themselves.

The on-chain tokenURI

Nothing is off-chain. tokenURI returns data:application/json;base64,... whose image field is itself data:image/svg+xml;base64,..., built by string concatenation from the piece's traits: a ceramic frame in one of three white finishes, a glass pane in two flat bands of one lime tint, a white character body clipped by the pane with a soft under-shade, and the headwear on top in solid deep green. Nothing is outlined; the only ink in the picture is the pair of eyes. There is no server and no IPFS pin to keep paying for. imageSVG and the pure imageForTraits it calls are view functions; because nothing they touch is reachable from a state-changing path, drawing one can never raise mint gas.

The hook slot

mintFromHook exists for a real Uniswap V4 afterSwap hook, which would mint inside the buy's own transaction, with no signature, no keeper and no gap. There was no Uniswap V4 PoolManager found on Robinhood Chain (id 4663) at the known addresses, which is why the signed path above is the one this collection actually uses today. If a PoolManager appears, the owner calls setHook once and both paths can run side by side; pieces minted either way are the same pieces, from the same seed rule and the same replay guard.

Security properties

Environment names

NameUsed byWhat it is
SIGNER_KEYthe signing route, the keeperThe key that signs mint authorisations. Its address must equal signer().
KEEPER_KEYthe keeper onlyThe key that pays gas to submit mints.
SNAG_RPC_URLbothRobinhood Chain RPC. Falls back to the first url in js/snag-config.js.

Contract addresses live in js/snag-config.js, not in the environment, and minBuy is always read from the contract, never from the site.

Addresses

ContractAddress
SnagCollectionnot published
$SNAG tokennot published
Poolnot published
Hooknot published