We kept the same swiss-army knife approach to editions that we used and loved from the original Zora Editions:
- the base implementation is extremely versatile and composable
- new editions are minimal proxies (i.e. cheaper to deploy than a brand new ERC721)
- creator-owned collections with a unique address and on-chain royalties
- each token is numbered with on-chain metadata rendering
- support for ETH sales
- support for minting through contracts (e.g. to support ERC20 primary sales)
... and then we added our on spin on it
Differences with the original Zora Editions
- support for time-limited editions
- support for opt-in OpenSea operator filtering
- support for
contractURI()
, which means a nicer out-of-the-box experience on OpenSea. The collection will be automatically configured with the artwork as the collection cover, and the EIP-2981 creator fees will also be reflected (throughseller_fee_basis_points
andfee_recipient
) - support for
setExternalUrl(string)
, reflected incontractURI()
andtokenURI(uint256)
- support for
setStringProperties(string[] names, string[] values)
, reflected intokenURI(uint256)
- special characters in the
name
anddescription
fields are escaped so that the JSON doesn't break in the contract and token URIs - an edition can be deployed at the same address on multiple chains (instead of being an auto-incrementing counter, the edition id is now a hash of
<creator-addr|edition-name|animation-url|image-url>
) - many gas efficiency improvements, particularly in the minting flow
- moved from hardhat to foundry
- solc upgraded from 0.8.6 to 0.8.16
- moved from OpenZeppelin's ERC721 as a base class to solmate's
- moved from OpenZeppelin's string libraries to Solady's (
LibString.toString(uint256)
,LibString.escapeJSON(string)
andBase64.encode(bytes)
) Edition
now inherits fromEditionMetadataRenderer
instead of using an externalSharedNFTLogic
library- Solidity errors instead of strings
- removed unnecessary functions from the ABI
See editions-gas-bench for the full methodology.
baseline | this fork | % change | |
---|---|---|---|
testCreateNewEdition() | 341563 | 225259 | -34.05% |
baseline | this fork | % change | |
---|---|---|---|
testMintByContract() | 57883 | 50702 | -12.41% |
testMintByOwner() | 48457 | 49869 | 2.91% |
testMintOpenEdition() | 67489 | 64612 | -4.26% |
💁♂️ we purposefully optimised for the mint-by-contract flow because this is the most common by far for us (i.e. a contract is an approved minter, e.g. to enforce some requirements such as an allowlist). We made the trade-off to deprioritize mints that come from the owner because it should be much less likely in practice.
baseline | this fork | % change | |
---|---|---|---|
testMint10ByContract() | 525498 | 515257 | -1.95% |
testMint10ByOwner() | 512988 | 511354 | -0.32% |
testMint10OpenEdition() | 515336 | 509392 | -1.15% |
baseline | this fork | % change | |
---|---|---|---|
testContractURI() | n/a | 42595 | |
testTokenURI() | 55674 | 42986 | -22.79% |
✨ credit to Solady's Base64.sol and LibString.sol for this improvement
baseline | Showtime editions | % change | |
---|---|---|---|
testTransferFrom() | 43090 | 40465 | -6.09% |
✨ credit to solmate's ERC721.sol for this improvement
TODO (not deployed yet)
Call EditionCreator.createEdition(...)
:
/// Creates a new edition contract as a factory with a deterministic address
/// Important: most of these fields can not be changed after calling
/// @param _name Name of the edition
/// @param _symbol Symbol of the edition
/// @param _description Description of the edition
/// @param _animationUrl Link to video for each token in this edition, ideally "ipfs://..."
/// @param _imageUrl Link to an image for each token in this edition, ideally "ipfs://..."
/// @param _editionSize Set to a number greater than 0 for a limited edition, 0 for an open edition
/// @param _royaltyBPS Royalty amount in basis points (1/100th of a percent) to be paid to the owner of the edition
/// @param _mintPeriodSeconds Set to a number greater than 0 for a time-limited edition, 0 for no time limit. The mint period starts when the edition is created.
/// @return newContract The address of the created edition
function createEdition(...) external override returns (IEdition newContract)
This will:
- deploy an EIP 1167 minimal proxy to the reference
Edition
implementation - transfer ownership of the edition to the
msg.sender
- emit a
CreatedEdition(uint256 editionId, address creator, uint256 editionSize, address newEdition)
event
After deploying an edition, call enableDefaultOperatorFilter()
to subscribe to the default OpenSea filter or setOperatorFilter(address operatorFilter)
to pick a different one
- limited edition: create it with
_editionSize > 0
- time limited open edition: create it with
_mintPeriodSeconds > 0
and_editionSize == 0
(after this duration, nobody will be able to mint) - a free mint for some accounts followed by a paid public mint: you will probably want to call
setApprovedMinter(allowListContractAddress, true)
to authorize mints through a contract that implements an allowlist, and then callsetSalePrice(uint256)
andsetApprovedMinter(0, true)
to kick off the paid public mint
# install foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# build
forge build
# test
forge test -vvv
# deploy dry run (replace with desired network)
source .env && forge script script/Deploy.s.sol --rpc-url mumbai
# deploy for real (replace with desired network)
source .env && forge script script/Deploy.s.sol --rpc-url mumbai --broadcast --verify
New base implementations can be redeployed with these separate contracts:
forge script script/Edition.s.sol --rpc-url <network> --broadcast --verify --watch
forge script script/SingleBatchEdition.s.sol --rpc-url <network> --broadcast --verify --watch
forge script script/MultiBatchEdition.s.sol --rpc-url <network> --broadcast --verify --watch