Docs menu

Trust model

What an integrator can rely on, what the admin can change, and what an external call into Shapes can do to your contract.

The reserve

address(this).balance >= redeemableBacking() + pendingFees()

Equality holds in normal use; ETH forced in by selfdestruct or block rewards is stranded above it. Direct transfers to the contract revert DirectDepositRejected. ETH enters only through the mint entrypoints and leaves through exactly three paths:

  1. Redemption and burn, which pay a destroyed token's backing to its owner or a chosen recipient.
  2. burnBacking, which sends an apex Shape's 100 ETH to 0x…dEaD.
  3. withdrawFees(recipient), which pays one recipient its own accrued mint fees.

There is no pause, upgrade, proxy, emergency withdrawal, asset recovery, allowlist, supply cap or royalty. royaltyInfo is declared and returns zero.

Roles

RoleReadCan
Token ownerownerOf(tokenId)Transfer, approve, redeem, burn, compose, decompose, split, burn backing
Collection ownerowner()Nothing. Attribution that follows the owner token
Adminadmin()Change presentation until locked, set and lock the two pointers, redirect future fees, set the fee within the cap, transfer or renounce the role
Artistartist()Nothing. Attribution of the deployer, plus one EIP-712 attestation

An approved ERC-721 operator can transfer a Shape but cannot redeem, burn or recompose it; those check msg.sender == ownerOf.

What the admin can change

SettingBoundFreeze
renderer()Must have code and support IShapeRenderer and IShapeGeometrylockPresentation()
collection() and its copyMust support IShapeCollection and report this Shapes from shapes()lockPresentation()
positions() pointerZero, or code answering ERC-165 for IShapePositionResolverlockPointer(0)
market() pointerZero, or code answering ERC-165 for IShapeAuctionHouselockPointer(1)
mintFee()At most unit() (0.01 ETH on mainnet)renounceAdmin()
feeRecipient()Not zero, not the Shapes addressrenounceAdmin()

A renderer change alters how tokens look, not what they are worth. Locking freezes the stored address, not the target's code. Check presentationLocked(), positions().locked and market().locked if your integration depends on a pointer staying put.

External calls Shapes makes

Shapes calls out in these places. Everything else is internal.

CallWhenEffect on the caller
onERC721Received on the recipientEvery mint (_safeMint to the caller or to), every split child, every restored decompose input, and safeTransferFromYour contract must implement IERC721Receiver to receive Shapes from these paths
ETH transfer to the redeemer or recipientredeem*, burnA recipient that reverts makes the redemption revert; the token survives
ETH transfer to a fee recipientwithdrawFeesOnly that recipient's withdrawal fails
renderer(), collection() and a new pointer targetMetadata views, plus one ERC-165 probe when the admin sets a pointerNever on a token state change
positionOf on the positions targetpositionOf(tokenId) only, with a 50,000 gas stipendA revert or bad return yields zero; no state change calls a pointer

Reentrancy

The mint, redemption, fee and recomposition entrypoints carry a reentrancy guard. Admin functions, attestArtist and the inherited ERC-721 transfer and approval functions do not. State is written before every receiver callback, so a callback sees the finished batch; during safeTransferFrom the receiver may redeem the Shape inside its own onERC721Received. Do not assume a token still exists after a safe transfer returns.

Presentation copy

The token name prefix and descriptions live on the collection contract and are validated so they cannot break the metadata JSON. A copy edit is two transactions, ShapeCollection.setMetadataCopy then Shapes.refreshMetadata, which emits ERC-4906 BatchMetadataUpdate and ERC-7572 ContractURIUpdated so marketplaces re-read.

Seeds are not randomness

Seeds derive from the token ordinal and block data and can be searched by minting more tokens at one fee per try. Never use a seed as a random source in your own contract.