A template library for secure NFT smart contracts development, including:
- NFT Marketplace, create and manage on-chain NFT orders and offers, pay with ETH or USDC.
- English Auction
- ERC-721 and ERC-1155 token presets:
- Clones "factory", deploy minimal proxy contracts aka "clones", simply and cheaply in an immutable way:
- Curated: a communal role-based access control factory, including a "curator" and a "minter" roles.
- Open: sovereign factory with single owner, public
clone
function, deploy copies of contract instances whitelisted by the owner. - Payable: sovereign factory with single owner, allowing anyone to clone whitelisted contracts for a fee.
Click the Use this template
button at the top of the Github page
in order to create a new repository from this project.
You may also install the template directly from the terminal using Foundry, if this is your first time with Foundry, check out the installation instructions:
forge init --template ninfa-labs/nft-suite my-nft-project
cd my-nft-project
bun install # install Solhint, Prettier, and other Node.js deps
Alternatively, if you have an existing project and just want to import the contracts as a library, install the contracts package using your preferred Solidity development framework:
# Using Foundry
forge install ninfa-labs/nft-marketplace
# Using Hardhat
npm install ninfa-labs/nft-suite
Foundry typically uses git submodules to manage dependencies, but this template uses Node.js packages because submodules don't scale.
This is how to install dependencies:
- Install the dependency using your preferred package manager, e.g.
bun install dependency-name
- Use this syntax to install from GitHub:
bun install github:username/repo-name
- Use this syntax to install from GitHub:
- Add a remapping for the dependency in remappings.txt, e.g.
dependency-name=node_modules/dependency-name
The following dependencies are included in package.json
:
- Forge Std: collection of helpful contracts and cheatcodes for testing
- Solhint: linter for Solidity code
- Prettier Plugin Solidity: code formatter for non-Solidity files
Note: Any external smart contract libraries, such as OpenZeppelin,
have been included in the src
directory as part of the local codebase, i.e. there is no lib
folder,
the original author and version are recorded within each contract's NatSpec comments.
This template comes with a set of sensible default configurations for you to use. These defaults can be found in the following files:
├── .editorconfig
├── .gitignore
├── .prettierignore
├── .prettierrc.yml
├── .solhint.json
├── foundry.toml
└── remappings.txt
This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull
request made to the main
branch.
You can edit the CI script in .github/workflows/ci.yml
.
To run all unit test files located in /test
:
forge test
To run a specific test file and test function:
forge test --match-path test/ERC721Base.t.sol --match-test testMint -vvvv
Get a gas report:
forge test --gas-report
Custom RPC endpoing:
forge test --fork-url=${RPC_URL_MAINNET} [...]
Deployment scripts are located in the script
folder. Currently it contains a single script, Deploy.sol
, which can be
used to deploy all contracts on any EVM compatible chain.
forge script script/Deploy.sol [...] --broadcast
The --broadcast
flag should only be used in order to deploy on the real testnet or mainnet, if the flag is omitted,
Foundry will use an RPC endpoint if provided and do a dry-run deployment, if no endpoint is provided it will deploy
on a local blockchain (Anvil).
Example using a custom RPC endpoint (local node):
forge script script/Deploy.s.sol --fork-url http://localhost:8545
For this script to work, you need to have a MNEMONIC
environment variable set to a valid
BIP39 mnemonic.
For more instructions on how to deploy to a testnet or mainnet, check out the Solidity Scripting tutorial.
The following paragraphs describe the inner workings of the factory deployment
Begin by deploying a factory contract (e.g., OpenFactory
) that creates minimal clones . This factory may manage:
- Optional cloning fees (e.g.,
FEE_BPS
,FEE_RECIPIENT
) - Whitelisting of approved master copies
- Cloning logic to deploy a proxy using
create2
(salted deterministic deployment) - Functions to predict the addresses of clones deployed using the deterministic method.
address FACTORY = new OpenFactory(FEE_BPS, FEE_RECIPIENT);
Deploy a master copy (an instance of ERC721Base
), passing the factory’s address to its constructor:
address ERC721_BASE_MASTER = new ERC721Base(address(FACTORY));
The constructor
of token contracts is used to set common state needed by cloned contracts, i.e. the address of the factory contract from which clones will be created (this means a factory instance must already exist because its address is needed as a constructor
argument by token contracts).
constructor(address factory_) {
_FACTORY = factory_;
}
- Stores the factory contract address.
- Ensures only that factory can invoke the clone’s initialization.
Why a Master Copy?
- Serves as the “implementation” contract.
- Each clone references this code via delegatecall, reducing gas versus a full deployment.
To allow the factory to clone a specific master copy, it must be whitelisted:
FACTORY.setMaster(address(ERC721_BASE_MASTER), true);
Once the master is whitelisted, call the factory’s clone
function:
address cloneAddress = FACTORY.clone(
address(ERC721_BASE_MASTER),
bytes32(0x0),
abi.encode(_MINTER, 1000, _SYMBOL, _NAME)
);
ERC721Base cloneInstance = ERC721Base(cloneAddress);
_instance
: Address of the whitelisted master copy._salt
: For deterministic CREATE2 deployment or0x0
if not needed._data
: Encoded parameters forinitialize(bytes)
.
After deployment, the factory calls the clone’s initialize(_data)
, which sets roles, royalties, and metadata.
The line require(msg.sender == _FACTORY);
is used for access control, it compares msg.sender
with the address of the factory contract that was set at deployment of the master contract instance. Therefore, all cloned contract instances will share the same factory address, without the need to set new access control state variables such as Openzeppelin's initializers every time a new clone is deployed.
function initialize(bytes memory _data) public virtual {
require(msg.sender == _FACTORY);
(address deployer, uint96 defaultRoyaltyBps, string memory symbol_, string memory name_) =
abi.decode(_data, (address, uint96, string, string));
symbol = symbol_;
_name = name_;
_setDefaultRoyalty(deployer, defaultRoyaltyBps);
_grantRole(DEFAULT_ADMIN_ROLE, deployer);
_grantRole(CURATOR_ROLE, deployer);
_grantRole(MINTER_ROLE, deployer);
_setRoleAdmin(MINTER_ROLE, CURATOR_ROLE);
}
- Access Restriction: Only the factory can call it (
require(msg.sender == _FACTORY)
). - Data Decoding: Extracts
deployer
,defaultRoyaltyBps
,symbol
,name
. - Role Assignment: Grants admin, curator, and minter roles to the
deployer
. - Default Royalty:
_setDefaultRoyalty(deployer, defaultRoyaltyBps)
.
Example command
forge verify-contract \
--chain-id 42 \
--num-of-optimizations 1000000 \
--watch \
--constructor-args $(cast abi-encode "constructor(string,string,uint256,uint256)" "ForgeUSD" "FUSD" 18 1000000000000000000000) \
--etherscan-api-key <your_etherscan_api_key> \
--compiler-version v0.8.10+commit.fc410830 \
<the_contract_address> \
src/MyToken.sol:MyToken
"Presets" are fully complete smart contracts that can be customized by overriding functions OR by importing "extensions" contracts.
These contracts integrate different Ethereum NFT standards (ERCs) with custom extensions modules, showcasing common configurations that are ready to deploy without having to write any Solidity code.
They can be used as-is for quick prototyping and testing, but are also suitable for production environments.
For example, ERC721Base
is a token preset, as it contains all standard function interfaces plus some optional ones,
here is the contract declaration for the ERC721Base
preset:
contract ERC721Base is AccessControl, ERC721Burnable, ERC721Royalties, ERC721Metadata_URI, ERC721Enumerable {
In the above line, all inherited contracts besides AccessControl
are extensions contracts, ERC721Enumerable
inherits
from the parent ERC721
contract and adds to it an optional extension of
ERC721
defined in the EIP that adds
enumerability of all the token ids in the contract as well as all token ids owned by each account and the.
All of the contracts are expected to be used either standalone or via inheritance by inheriting from them when writing your own contracts. See the tutorials section for details each contract.
Extensions provide a way for you to pick and choose which individual pieces you want to put into your contract; with
full customization of how those features work. These are available at src/token/ERC721/extensions/
and
src/token/ERC1155/extensions/
depending on the token standard.
Extensions are simply contracts that are meant to be inherited by implementations and their functions either called or
overridden by the child implementation. Token preset contracts are an example of how exentions should be implemented,
see src/token/ERC721/presets
and src/token/ERC1155/presets
.
Alternatively, users can use these as starting points when writing their own contracts, extending them with custom functionality as they see fit.
1. To start, import and inherit a preset contract.
2. Preset contracts expect certain constructor arguments to function as intended. Implement a constructor for your
smart contract and pass the appropriate values to a constructor for the base contract. You also MAY want to override the
initialize
function, for example the preset contract ERC721LazyMint
inherits a library EIP712
thus extending the
base preset contract and overrides the initialize function in order to initialize the inherited contract as well.
contract ERC721LazyMint is ERC721Base, EIP712 {
function initialize(bytes memory _data) public override(ERC721Base, EIP712) {
ERC721Base.initialize(_data);
// initialize Base contract before EIP712 because
// "name" metadata MUST to be set prior calling EIP712's initialize()
EIP712.initialize("");
}
constructor(address factory_) ERC721Base(factory_) { }
}
Inheritance allows you to extend your smart contract's properties to include the parent contract's attributes and properties. The inherited functions from this parent contract can be modified in the child contract via a process known as overriding.
All NFT preset contracts in ./src/token
are compatible with both upgradeable and regular deployment patterns. All
initial state changes are written inside the initialize()
function, rather than the constructor, this is so that
contract specific parameters can be set when deploying new sovereign contracts (clones) from a factory contract.
Therefore, even though the clones are not really upgradeable, they have most of the same requirements that upgradeable
contracts have: initializer function, no immutable variables.
Found a security issue with our smart contracts? Send bug reports to [email protected] and we'll continue communicating with you from there.
If you have any feedback, please reach out to us at [email protected].