ERC-721 is the main standard to represent non-fungibles tokens (NFT) on Ethereum and EVM blockchain. This ERC defines the functions, events and the behavior of a token implementing this interface.
One of the main libraries used to build ERC-721 contract is OpenZeppelin. This library provides already all functions which are part of the standard. Nevertheless, OpenZeppelin does not provide a deployable contract, but only an abstract contract which can be used to build other contracts though inheritance but cannot be deployed directly on the blockchain. You can find more information about their implementation in their documentation.
TERC-721 aims to provide a minimal deployable implementation for standalone deployment (immutable) and proxy deployment (upgradeable) which allows the issuer (and only him) to mint and burn tokens.
TERC-721 exists in two different version: standalone and proxy:
TERC721Standalone
for an immutable deployment, without proxy
TERC721Upgradeable
for an upgradeable deployment, with a compatible proxy (Transparent or Beacon)
[TOC]
In addition to ERC-721, TERC-721 uses the following ERCs:
- ERC-4906: emit
BatchMetadataUpdate
when the baseURI is updated - ERC-6093: Custom errors for ERC-721 tokens (through OpenZeppelin)
- eip-3643: implements the following function:
version()
- TERC721Upgradeable only:
- implements ERC-7201 to manage the storage location.
These ERC-721 tokens have the following characteristics:
Mint
-
Mint functions only accessible with the MINTER role
-
Batch mint functions only accessible with the MINTER role
Burn
- A burn function only accessible with the BURNER role
- A batch burn function only accessible with the BURNER role
ERC721
-
At deployment, the issuer can set the name, symbol and the baseURI
-
Once deployed, it is no longer possible to modify the name and symbol except via an upgrade in the case of the proxy. A setter function is available to set the
baseURI
again.
There are three roles: MINTER_ROLE, BURNER_ROLE and DEFAULT_ADMIN_ROLE
The DEFAULT_ADMIN_ROLE has all the roles by default
Contract | Type | Bases | ||
---|---|---|---|---|
β | Function Name | Visibility | Mutability | Modifiers |
TERC721Standalone | Implementation | TERC721Share, TERC721StandaloneBurn, TERC721StandaloneMint | ||
β | Public βοΈ | π | ERC721 | |
β | setBaseURI | Public βοΈ | π | onlyRole |
β | baseURI | Public βοΈ | NOβοΈ | |
β | supportsInterface | Public βοΈ | NOβοΈ | |
β | hasRole | Public βοΈ | NOβοΈ | |
β | _setBaseURI | Internal π | π | |
β | _baseURI | Internal π |
Contract | Type | Bases | ||
---|---|---|---|---|
β | Function Name | Visibility | Mutability | Modifiers |
TERC721StandaloneMint | Implementation | ERC721, AccessControl, TERC721ShareMint | ||
β | mintTokenId | Public βοΈ | π | onlyRole |
β | batchMintTokenIds | Public βοΈ | π | onlyRole |
β | batchMintTokenIds | Public βοΈ | π | onlyRole |
β | nextTokenId | Public βοΈ | NOβοΈ | |
β | setNextTokenId | Public βοΈ | π | onlyRole |
β | mint | Public βοΈ | π | onlyRole |
β | batchMint | Public βοΈ | π | onlyRole |
β | batchMint | Public βοΈ | π | onlyRole |
β | supportsInterface | Public βοΈ | NOβοΈ | |
β | _mintAndEvent | Internal π | π |
Contract | Type | Bases | ||
---|---|---|---|---|
β | Function Name | Visibility | Mutability | Modifiers |
TERC721StandaloneBurn | Implementation | ERC721, AccessControl, TERC721ShareBurn | ||
β | burn | Public βοΈ | π | onlyRole |
β | batchBurn | Public βοΈ | π | onlyRole |
β | supportsInterface | Public βοΈ | NOβοΈ |
Contract | Type | Bases | ||
---|---|---|---|---|
β | Function Name | Visibility | Mutability | Modifiers |
TERC721Upgradeable | Implementation | Initializable, TERC721Share, TERC721UpgradeableBurn, TERC721UpgradeableMint | ||
β | Public βοΈ | π | NOβοΈ | |
β | initialize | Public βοΈ | π | initializer |
β | __TERC721Upgradeable_init_unchained | Internal π | π | onlyInitializing |
β | setBaseURI | Public βοΈ | π | onlyRole |
β | baseURI | Public βοΈ | NOβοΈ | |
β | hasRole | Public βοΈ | NOβοΈ | |
β | supportsInterface | Public βοΈ | NOβοΈ | |
β | _setBaseURI | Internal π | π | |
β | _baseURI | Internal π | ||
β | _getTERC721UpgradeableStorage | Private π |
Contract | Type | Bases | ||
---|---|---|---|---|
β | Function Name | Visibility | Mutability | Modifiers |
TERC721UpgradeableMint | Implementation | ERC721Upgradeable, AccessControlUpgradeable, TERC721ShareMint | ||
β | mintTokenId | Public βοΈ | π | onlyRole |
β | batchMintTokenIds | Public βοΈ | π | onlyRole |
β | batchMintTokenIds | Public βοΈ | π | onlyRole |
β | nextTokenId | Public βοΈ | NOβοΈ | |
β | setNextTokenId | Public βοΈ | π | onlyRole |
β | mint | Public βοΈ | π | onlyRole |
β | batchMint | Public βοΈ | π | onlyRole |
β | batchMint | Public βοΈ | π | onlyRole |
β | supportsInterface | Public βοΈ | NOβοΈ | |
β | _mintAndEvent | Internal π | π | |
β | _getTERC721UpgradeableMintStorage | Private π |
Contract | Type | Bases | ||
---|---|---|---|---|
β | Function Name | Visibility | Mutability | Modifiers |
TERC721UpgradeableBurn | Implementation | ERC721Upgradeable, AccessControlUpgradeable, TERC721ShareBurn | ||
β | burn | Public βοΈ | π | onlyRole |
β | batchBurn | Public βοΈ | π | onlyRole |
β | supportsInterface | Public βοΈ | NOβοΈ |
Symbol | Meaning |
---|---|
π | Function can modify state |
π΅ | Function is payable |
The toolchain includes the following components, where the versions are the latest ones that we tested:
- Foundry
- Solidity 0.8.28 (via solc-js)
- OpenZeppelin Contracts (submodule) v5.3.0
- OpenZeppelin Contracts upgradeable (submodule) v5.3.0
See report made by SecFault Security
The audit was performed on the version 0.2.0 and the version containing the fix is the version 1.0.0
The functions setNextTokenId
and nextTokenId
have been added in the version 1.0.0 and therefore were not included in the audit.
See crytic/slither
slither . --checklist --filter-paths "openzeppelin-contracts|openzeppelin-contracts-upgradeable|test|forge-std" > slither-report.md
myth analyze src/TERC721Standalone.sol --solc-json solc_setting.json
aderyn --output report.md
See Cyfrin/aderyn
See ./doc/script and Consensys/surya
npx prettier --write --plugin=prettier-plugin-solidity 'src/**/*.sol'
npx prettier --write --plugin=prettier-plugin-solidity 'test/**/*.sol'
See ./doc/script
Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.
Foundry consists of:
- Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
- Chisel: Fast, utilitarian, and verbose solidity REPL.
Explain how it works.
The contracts are developed and tested with Foundry, a smart contract development toolchain.
To install the Foundry suite, please refer to the official instructions in the Foundry book.
You must first initialize the submodules, with
forge install
See also the command's documentation.
Later you can update all the submodules with:
forge update
See also the command's documentation.
The official documentation is available in the Foundry website
forge build
You can run the tests with
forge test --ffi
To run a specific test, use
forge test --match-contract <contract name> --match-test <function name>
See also the test framework's official documentation, and that of the test commands.
- Perform a code coverage
forge coverage
- Generate LCOV report
forge coverage --report lcov
- Generate
index.html
forge coverage --ffi --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage
See Solidity Coverage in VS Code with Foundry & Foundry forge coverage
The burn mechanism implemented in TERC721 is not compatible with the OpenSea guideline
We donβt allow NFTs that are guaranteed to be burned or burned at the full discretion of the creator. There must be an element of randomization, chance, or an event that triggers the burn
See OpenSea support - How does OpenSea handle NFTs with a burn mechanism?
The contract emits the event BatchMetadataUpdate
when the baseURI is updated as supported by OpenSea to refresh token metadata.
To refresh a whole collection, emit
_toTokenId
withtype(uint256).max
See docs.opensea - metadata updates
The original code is copyright (c) Taurus 2025, and is released under MIT license.