User journeysCreate a collection

Create a collection

Registers a new ERC-721 collection on the Locker, deploying a paired CollectionToken ERC-20 clone in the same call. The collection is not yet tradable — pool initialization is a separate transaction (Initialize a collection).

Prerequisites

  • The collection address is a real ERC-721. The Locker calls supportsInterface(0x80ac58cd) and rejects anything that doesn’t claim it.
  • The collection has never been sunset on this Locker. Sunsetted addresses are permanently rejected (CollectionPreviouslySunset).
  • If Locker.permissionlessCreateCollection() == false, the caller is the Locker owner.
  • msg.value == Locker.collectionCreationFee() exactly. Both 0 and non-zero values are valid; non-matching values revert with CollectionCreationFeeDoesNotMatch.
  • The Locker is not paused (Locker.paused() == false).

Inputs

ParamTypeMeaning
_collectionaddressERC-721 contract address
_namestringDisplay name for the new CollectionToken
_symbolstringSymbol for the new CollectionToken

Contract calls

#CallerCallNotes
1Anyone (or owner)Locker.createCollection{value: fee}(collection, name, symbol)Returns the deployed CollectionToken address

Events emitted in order:

  1. Locker.CollectionCreated(collection, collectionToken, name, symbol, creator)
  2. The Locker forwards (collection, collectionToken) to NFTXV4Hook.registerCollection(collection, collectionToken) (no event).

Code

createCollection.ts
import { lockerAbi } from "@nftx/contracts";
import { NFTX_CONTRACTS } from "@nftx/config";
import {
  createPublicClient,
  createWalletClient,
  custom,
  http,
  parseEther,
} from "viem";
import { base } from "viem/chains";
 
const publicClient = createPublicClient({ chain: base, transport: http() });
const walletClient = createWalletClient({
  chain: base,
  transport: custom(window.ethereum!),
});
 
const [account] = await walletClient.getAddresses();
const locker = NFTX_CONTRACTS[base.id].locker;
 
const fee = await publicClient.readContract({
  address: locker,
  abi: lockerAbi,
  functionName: "collectionCreationFee",
});
 
const { request } = await publicClient.simulateContract({
  account,
  address: locker,
  abi: lockerAbi,
  functionName: "createCollection",
  args: ["0xCollection", "Cool Cats Floor", "fCATS"],
  value: fee,
});
 
const txHash = await walletClient.writeContract(request);
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

Failure modes

ErrorCauseResolution
InvalidERC721Address doesn’t return true for supportsInterface(0x80ac58cd)Confirm you’re passing the NFT contract, not the OpenSea proxy
CollectionAlreadyExists_collection already registeredLook up the existing CollectionToken via Locker.collectionToken(_collection)
CollectionPreviouslySunsetAddress was sunset previouslyMigrate to a new ERC-721 address
CreateCollectionRestrictedToOwnerpermissionlessCreateCollection == false and caller isn’t ownerWait for the toggle, or ask the owner
CollectionCreationFeeDoesNotMatch(received, required)msg.value differs from collectionCreationFeeRe-read the fee just before sending
Paused()Locker is pausedRetry once unpaused
⚠️

createCollection is not idempotent — the receipt’s deployed CollectionToken address changes per-call. Index Locker.CollectionCreated rather than re-deriving the clone address.

Subgraph follow-through

A Collection row appears in the subgraph keyed by ${chainId}-${collection}. Until Initialize a collection succeeds:

  • poolKey, sqrtPriceX96, tick, liquidity, swapFee, protocolFee remain null/zero.
  • defaultFee is the Config.defaultFee for the chain.
  • publicListings == 0.
query NewCollection($chainId: Int!, $collection: String!) {
  Collection(
    where: {
      chainId: { _eq: $chainId }
      collection: { _eq: $collection }
    }
    limit: 1
  ) {
    id
    collectionToken
    name
    symbol
    creator
    createdAt
    poolKey   # null until initializeCollection
  }
}