Building on Shapes
Patterns for a contract that holds, moves or reshapes Shapes, and the hooks Shapes offers to an ecosystem contract.
Receive Shapes
Every path that gives your contract a Shape uses _safeMint or safeTransferFrom: mintTo, mintBatchTo, splitTo, decomposeTo, and any user transfer. Implement IERC721Receiver and return its selector, or the call reverts ERC721InvalidReceiver.
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data)
external returns (bytes4)
{
require(msg.sender == address(SHAPES), "not Shapes");
// Shapes has finished writing state: valueOf(tokenId) and totalSupply() are final here.
return IERC721Receiver.onERC721Received.selector;
}
operator is the account that called Shapes; from is zero for a mint, split child or decompose restore. The hook may call back into Shapes; the mutating entrypoints carry a reentrancy guard, so a nested mint or redeem inside a mint or decompose reverts, while a redeem inside a plain safeTransferFrom succeeds.
Act on Shapes you hold
redeem, burn, compose, decompose, split and burnBacking check msg.sender == ownerOf. Approval is not enough. A contract that manages Shapes for users must hold them itself, or have the user act. A vault that holds a Shape can redeem it and receive the ETH in receive, compose it with others it holds, or split it and receive the children.
redeemTo, redeemBatchTo, splitTo and decomposeTo let the holder direct the output to another address in one call, so a vault can redeem straight to its user.
Pick a denomination
Convert user amounts with the ladder reads rather than a local table, so the same code runs on Sepolia's scaled ladder:
uint8 n = SHAPES.denominationCount(); // 9
for (uint8 i = 0; i < n; ++i) {
if (SHAPES.denominationAt(i) == amountWei) { /* index i */ }
}
isSupportedDenomination(amountWei) is the yes/no form. To make change, split into indices; to consolidate, compose into a survivor whose summed backing is a denomination.
Check what a contract is
supportsInterface distinguishes the Shapes token from any other ERC-721 and advertises the slices of the surface. A payment contract that only redeems can check IShapeValue; a recomposition workflow checks IShapeRecomposition. Ids are on Interfaces.
Read before you act
shapeState(tokenId) gives every fact in one call. exists(tokenId) never reverts. previewCompose and previewSplit show outcomes; simulate the mutating call from the acting account to test ownership. Read mintFee() inside the transaction that pays it.
Positions
Shapes carries an optional positions pointer: an address the admin sets and can lock, answering IShapePositionResolver:
interface IShapePositionResolver {
function positionOf(uint256 tokenId) external view returns (address);
}
Shapes.positionOf(tokenId) forwards to it with a 50,000 gas stipend and returns zero on any failure. It is a discovery hook: a way for the canonical positions layer, once one exists, to say where a Shape's external position lives. Shapes never calls the target on a state change and gives it no authority. Read positions() for (target, locked); zero target means none is configured. Anyone may build a positions contract; the pointer names the canonical one.
The market pointer and the auction house
market() names the canonical auction house, ShapeAuctionHouse, an independent contract with no authority over Shapes. It runs English auctions for any ERC-721 in which bids are sets of Shapes: a bid escrows cards whose backing sums to the bid amount, in 0.01 ETH units, and the house can mint cards from ETH for a bidder. Outbid cards are pulled back by the bidder, the lot by the winner, the winning cards by the seller. The interface is IShapeAuctionHouse (createAuction, bid, settle, claimLot, withdraw, claimProceeds, minimumBid, getAuctionFor, hasAuctionFor). Its full NatSpec is on the Contracts page. The owner token #0 was listed in auction 0 at deployment.
Presentation contracts
renderer() and collection() are readable by anyone. A contract can render any card from raw inputs through IShapeRenderer and IShapeGeometry, and IShapeCollection.cardFor(seed, denomIndex) previews a card no token exists for. See Geometry and rendering.
Before you rely on it
- Approval is trust. An approved operator can transfer a Shape to itself and redeem it, so approving a contract for a Shape hands over that Shape's full ETH value. Ask for the minimum approval a flow needs, and expect users to be asked the same.
- Price by value, not traits. Artwork, ink gene and provenance carry no redemption value.
redeemableValueWeiis the number. - Give value-moving calls gas headroom. Every state-changing entrypoint runs behind a reentrancy guard whose slot is reset at the end of the call, earning a refund.
eth_estimateGasreports the amount net of that refund, slightly below what the execution needs mid-flight, so a contract or script that forwards a hard gas limit must add a margin over the bare estimate. Wallets add one automatically. - Never accept a Black Shape as payment.
valueOfis zero andisBlackis true. - Artist status is not authority.
artist(),artistSignature()andartistReleaseHash()are attribution. An ERC-1271 attestation was valid whenattestArtistran; a later wallet change can make a fresh check fail.
Things Shapes will not do
- Hold a Shape itself. Minting or transferring to the Shapes address reverts.
- Accept ETH outside a mint. Direct transfers revert.
- Pay royalties.
royaltyInforeturns zero. - Call your contract on a state change, other than the ERC-721 receiver hook and the redemption payout.
- Freeze, escrow, wrap or seize a token. Those are for contracts built on top.