Contracts
The reference implementation is small by design: a handful of interfaces, storage libraries, logic libraries, and extension mixins, assembled by inheritance into six concrete, deployable token contracts — three strict twin pairs, one per project shape — nothing else accomplishes the whole feature set.
A Series is many unique tokens (ERC-721). An Edition is many copies of a token (ERC-1155). Uniqueness maps to ERC-721, copies map to ERC-1155 — an orthogonal choice from the project shape (one artwork, many artworks, or generative/code) and from everything else a project picks.
Extensions are mixins, not a framework
Every opt-in piece of the event spine — Royalty, On-Chain Metadata, Max
Invocations, and the rest — is one abstract contract in extensions/<name>/, and it is the whole
extension: its private ID/VERSION constants, its _init<Name>(...) setup hook, its events (declared
on a paired IAbx<Name> interface), and its own supportsInterface override. Nothing about an
extension lives anywhere else, so adding one never touches a shared file. RoyaltyExtension is
representative:
abstract contract RoyaltyExtension is AbxBeaconCore, ERC2981, Ownable, IAbxRoyalty {
bytes32 private constant ID = 0x09e6...; // keccak256("abx.extension.royalty")
uint16 private constant VERSION = 1;
function _initRoyaltyExtension(address receiver, uint16 basisPoints) internal {
_setExtensionVersion(ID, VERSION); // announce via the beacon
_setRoyalty(receiver, basisPoints);
}
function supportsInterface(bytes4 interfaceId)
public view virtual override(AbxBeaconCore, ERC2981) returns (bool)
{
return AbxBeaconCore.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId);
}
}A concrete token composes the mixins it needs and explicitly ORs each one's supportsInterface —
Solady's leaf implementations don't super-chain, so composition is spelled out, not implicit, in the
most-derived contract. ID is private on purpose: two extensions can never collide, even composed in
the same token.
One extension branches its ERC-165 answer on state, deliberately: CreatorToken (opt-in
ERC-721C) advertises the creator-token ids (0xad0d7f6c /
0xa07d229a) and its beacon version only when the token enrolled at deploy — a permanent,
deploy-time choice — so an unenrolled token is indistinguishable from a pre-721C token.
Namespaced storage: no layout to protect
Every concern with real state — the beacon's extension registry, royalty, params, on-chain script,
whatever — gets its own ERC-7201 namespace: a library with a
Layout struct at a slot computed once, off any inheritance position.
library BeaconStorage {
struct Layout {
mapping(bytes32 => uint16) extensionVersion;
}
// keccak256(abi.encode(uint256(keccak256("abx.storage.beacon")) - 1)) & ~bytes32(uint256(0xff))
bytes32 internal constant STORAGE_SLOT = 0xfa76...;
function layout() internal pure returns (Layout storage l) {
assembly { l.slot := STORAGE_SLOT }
}
}Because the slot is a hash of the library's own name, not a position in an inheritance chain, mixins can be added, removed, or reordered on a concrete token with zero risk of two of them silently overlapping the same storage — "no reliance on inheritance-layout order anywhere in an ABX contract" is a repeated, literal comment across the codebase. It also enables the other half of the library story below: a shared, separately-deployed library can safely write into the calling token's own storage, because it computes the identical namespaced slot the token itself would.
Two kinds of libraries
- Storage libraries (17:
BeaconStorage,SupplyStorage,CollectionMetadataLib,TokenURIStorage,ContractURIStorage,ParamsStorage,ConfigurableParamsStorage,OnChainMetadataStorage,OnChainScriptStorage,DependenciesStorage,MaxInvocationsStorage,ExternalMinterStorage,PrimaryPayeeStorage,PausedStorage,SeedSourceStorage,SeriesMintStorage,TransferValidatorStorage) — nothing but aLayoutstruct and alayout()accessor. If a library has one of these, it holds state; every other library doesn't. - Logic libraries — stateless helpers, split by how they run:
- Inlined (
TokenDataLib,DynamicBuffer) —internalfunctions, compiled straight into the caller, no separate deployment.AbxVersionis a further degenerate case: a single constant, no functions at all. - Delegatecalled (
AbxParamsLib,AbxCodeLib) —publicfunctions, which Solidity always compiles as calls to a separately-deployed copy of the library (forge auto-deploys and links them).Params/ConfigurableParamsroute throughAbxParamsLib;OnChainScript/Dependenciesroute throughAbxCodeLib. Both are externalized for the same stated reason: "the spine is identical to an inlined implementation; EIP-170 is why it's a library" —SeriesCodecomposes all four of these extensions at once, and inlining that much logic risks the 24,576-byte contract-size ceiling (the same ceiling that bounds one SSTORE2 chunk).AbxParamsLibalso carries the params key-enumeration and schema read views: the mixin shells forward their raw calldata (same signature, same selector) and return the library's return data untouched, so the library's read signatures are part of the tokens' external ABI. Delegatecall preserves the caller's storage context, so the externalized logic still reads and writes the token's own namespaced state — never any of its own.
- Inlined (
Even with both libraries externalized, SeriesCode still rides the ceiling: its
implementation, its factory, and the on-chain generator singleton (AbxGenerator) all compile at a
separately configured, lower optimizer setting (200 runs, against 1,000,000 everywhere else) purely to
shrink further and fit. Every other contract optimizes for cheap runtime gas instead, on the reasoning
that a clone, once deployed, runs forever.
Six contracts, three strict twins
The reference implementation ships one contract per (shape, standard) pair:
| Shape | ERC-721 (unique) | ERC-1155 (copies) |
|---|---|---|
| One artwork | OneOfOneImage | OneOfOneEdition |
| Many artworks | SeriesImage | EditionImage |
| Generative / code | SeriesCode | EditionCode |
Each column is a strict-superset ladder — a later contract is textually the earlier one's
is (...) list with more mixins appended: OneOfOneImage/OneOfOneEdition compose the minimal set
that can hold on-chain fields and a royalty; SeriesImage/EditionImage add what a sized, sellable
drop needs — a supply cap, a delegatable minter, a payout address, a pause gate; SeriesCode/
EditionCode add what a generative drop needs on top of that — mint-time seeds, governed PostParams,
and on-chain code custody. Each row is a twin pair sharing the identical extension surface, because
every ABX extension mixin inherits AbxBeaconCore, not the ERC-721 or ERC-1155 base — nothing about an
extension assumes a standard.
Every concrete contract's initialize() calls each mixin's _init<Name> in one fixed order, and the
doc comment pins the exact resulting event sequence — for example SeriesCode's: AbxDeployed → AbxExtensionVersionSet(royalty) → RoyaltyChangedForAll → [AbxExtensionVersionSet(creator-token) → TransferValidatorUpdated] → … → AbxExtensionVersionSet(dependencies) → TokenFieldSet* → ContractURIUpdated → Transfer (the bracketed pair appears only when the token enrolls as a creator
token at deploy). An indexer author can read the sequence straight off the source instead of inferring
it from tests. EditionCode's sequence is the same shape, one rung over: TransferSingle in place of
Transfer at the end, everything else identical.
What's shared, unchanged, between a twin pair
- Every extension above the base itself:
RoyaltyExtension,MaxInvocations(row 2+),ExternalMinter,PrimaryPayee,Paused,OnChainMetadata,SeedSourceExtension,ConfigurableParams,OnChainScript,Dependencies(row 3) — composed unchanged onto either base. AbxMetadataRenderer,AbxGenerator,AbxChunkStore,AbxSeedSource— the shared, per-chain singletons — read a token only through its extension interfaces plusICollectionName.name(), so the exact same deployed renderer serves a 721 token and its 1155 twin: no per-standard renderer, no redeploy for editions.- PostParams, except one leg. The
TokenOwnerauth leg generalizes from "theownerOfholder" to "any holder withbalanceOf(id) > 0" on an edition — params are per-id shared state of the artwork, and last-writer-wins among holders is the intended semantic — but delegate.xyz delegation does not carry over: an ERC-1155 id has no enumerable single holder to check a delegate against, so an edition's holders always configure directly, never through a vaulted delegate.
What differs
- Per-id supply. An edition adds
EditionSupply—totalSupply(id)/maxSupply(id)/setMaxSupply(id, cap)— the only extension id new to editions (abx.extension.edition-supply); every other extension id above is shared, unchanged, with the 721 side.MaxInvocationskeeps its 721 meaning unchanged on the multi-id twins: it still caps the id space (how many distinct artworks may ever exist), not any one artwork's copy count. - The metadata-refresh signal. A 721 twin pings ERC-4906's
MetadataUpdate; an edition has no such standard (IERC4906 is IERC721) and instead emits ERC-1155's nativeURI(string, uint256)per id — see Event spine. - The mint primitive. A 721 twin (past the 1/1) exposes
IAbxSequentialMint; every edition exposesIAbxEditionMint—mint(to, id, amount), an identified id with a per-mint amount, not a generalization of the sequential primitive. See Minting. - The creator-token flavor.
CreatorToken(ERC-721C) andCreatorToken1155(ERC-1155C) share the exact same ERC-165 ids and beacon extension id, but the validator call differs: 721C's is a view with no amount, 1155C's carries the transferred amount and isn't a view — called once per(id, amount)pair in a batch transfer. See Royalty enforcement. - The
OneOfOneEditionsale-stack asymmetry.OneOfOneImageis one-shot and owner-only, with no sale stack at all.OneOfOneEditionshipsExternalMinter/PrimaryPayee/Pausedfrom day one — a rungOneOfOneImagenever climbs — because a priced open or limited edition of a single artwork is the dominant 1155 product, sold through its own sibling minter,AbxFixedPriceMinter1155.
One implementation, many clones
Each concrete token has one sibling factory (OneOfOneImageFactory, SeriesImageFactory,
SeriesCodeFactory, and their edition twins OneOfOneEditionFactory, EditionImageFactory,
EditionCodeFactory), and all six are the same shape:
- The factory deploys one implementation in its own constructor, which immediately calls
_disableInitializers()— so the master copy can never be initialized (or hijacked) directly. deploy(params)clones it via EIP-1167 (Solady'sLibClone.clone) and callsinitialize(params)on the fresh clone in the same transaction.deployDeterministic(params, salt)clones to a predictable address (LibClone.cloneDeterministic), guarded by the salt's leading 20 bytes — all-zero is permissionless, a specific address must matchmsg.sender— so a reserved address can't be front-run.- The factory is ownerless and immutable: no admin key, no upgrade path, nothing to rug. It is
the only writer of its own
isAbxClonemapping, which is the trust anchor — verifying a contract is canonical means verifying the factory deployed it, not trusting the spoofableAbxDeployedbeacon (see Authenticity).
Renderers and readers: composed by address, not by inheritance
AbxMetadataRenderer, AbxChunkStore, AbxGenerator, and AbxSeedSource are a different idiom
entirely: stateless, ownerless, shared singletons, deployed once per chain and referenced by address
from a field's or a contract's stored value — never inherited into a token. A mixin composes in; a
renderer, reader, or seed source composes alongside, staticcalled at read time
(Field renderers), called through IAbxOnChainReader
(On-chain storage), or called once at mint
(Code projects). Keeping them outside the token's inheritance tree
is what lets one deployment serve every token on the chain, of any concrete type, and be swapped out —
by pointing a new address — without touching the token itself.
Read next
Event spine, Metadata, On-chain storage, Parameters, Code projects