SNAG docs, for developers
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.
mintForBuy, callable by anyone, normally the keeper:
function mintForBuy(
address buyer,
bytes32 buyRef,
uint256 amountIn,
uint256 deadline,
bytes calldata signature
) external whenNotPausedRequires 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 whenNotPausedOnly 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.
| Function | Returns |
|---|---|
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. |
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.
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);error BadSignature();
error AuthorizationExpired();
error BuyAlreadyUsed();
error BelowMinBuy();
error ZeroBuyer();
error NotHook();
error SettingsFrozen();
error SupplyExhausted();
error RenounceDisabled();
error ZeroAddress();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.
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.
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.
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.
usedBuy[buyRef] is written before the mint, on both minting paths, so no buy reference can ever produce two pieces.tx.origin. Every authorisation check uses the recovered signer against a message hash; nothing here trusts the top-level transaction sender._mint, not _safeMint. A buyer contract with a reverting or gas-burning onERC721Received could otherwise block its own piece, or worse, re-enter or grief the keeper paying for the transaction. A buy is not a request for a callback.token, pool and minBuy can only be set before the first mint. After that, they are fixed for the life of the contract.| Name | Used by | What it is |
|---|---|---|
SIGNER_KEY | the signing route, the keeper | The key that signs mint authorisations. Its address must equal signer(). |
KEEPER_KEY | the keeper only | The key that pays gas to submit mints. |
SNAG_RPC_URL | both | Robinhood 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.
| Contract | Address |
|---|---|
| SnagCollection | not published |
| $SNAG token | not published |
| Pool | not published |
| Hook | not published |