Nonfungible ERC1155 implementation #4684
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
As the ERC-1155 multi-token standard gains wider adoption by web3 platforms, using ERC-1155 for nonfungible tokens is gaining appeal, despite ERC-721 being more of a natural fit for tokens that can only have one owner. This is mostly because of ERC-1155 being more gas-efficient than ERC-721 as it tracks less information on-chain, and also because of other dev-friendly features like the URI-template scheme standardized by ERC-1155.
An ERC-1155 contract that enforces nonfungibility could be implemented naively using
openzeppelin-contracts
as follows:However this is gas-inefficient because on every transfer, not only is the contract internally modifying both
_balances[id][from]
and_balances[id][to]
(inERC1155
grandparent), but the contract also needs to track the_totalSupply[id]
(inERC1155Supply
parent) in order to be aware of whether or not a token already exists.Moreover
ERC1155Supply
also tracks_totalSupplyAll
, which is not really required for a nonfungible ERC-1155 implementation. This can easily be remedied by deleting anytotalSupplyAll
-related code from theERC1155Supply
parent. Let’s call this implementationERC1155NonfungibleNaiveLite
.But
ERC1155NonfungibleNaiveLite
is still wasteful, and it turns out to be much more efficient if the nonfungibility constraint is exploited to simplify the internal data model to a model that for each unique minted token tracks its single owner (as in ERC-721), rather than tracking multiple balances per token.Indeed such a nonfungible ERC-1155 implementation, that tracks ownership rather than balances, has already been developed in the ERC1155D project.
ERC1155D
reimplements ERC-1155 from scratch, optimizing heavily for gas efficiency. On the other hand, in this PR we are proposing an alternative implementation, namedERC1155Nonfungible
, that is written as an extension ofopenzeppelin-contract
’sERC1155
. LikeERC1155D
,ERC1155Nonfungible
also cuts down gas usage significantly by tracking ownership instead of balances, butERC1155Nonfungible
focuses less on squeezing out up to the last drop of gas, and more on having a more concise implementation that fits better into thisopenzeppelin-contracts
library.To evaluate
ERC1155Nonfungible
, we create a simple test where we mint a token toaccount1
, then transfer it fromaccount1
toaccount2
, and then burn the token. The test was run with all the different ERC-1155 implementations mentioned above, and also with theERC721
contract, and the following gas usage was recorded:ERC1155Nonfungible
andERC1155D
outperform all the other implementations significantly, which demonstrates that for nonfungible tokens it is worthwhile to reimplement using an ownership data model, rather than just enforce the nonfungible constraint onto the balances model. Between the winners,ERC1155D
performs marginally better thanERC1155Nonfungible
, asERC1155Nonfungible
sacrifices some gas optimization, but in exchange for being ~5 times more compact and fitting better into theopenzeppelin-contracts
library architecture.As far as the
ERC1155Nonfungible
implementation is concerned, some arbitrary decisions were taken:ERC1155Nonfungible
with an alternative storage implementation, e.g. to store more token information along with the owner (even within the same storage slot if it can fit in 96 bits)ownerOf()
is exposed externally, and it returns zero-address for a non-existent token, rather than reverting (as in ERC-721)ERC1155NonfungibleDuplicate
. If we are to followERC721
’s lead, we would flag this error condition by reverting withERC1155InvalidSender(address(0))
, but it seems more clear to create a dedicated error.If there is interest to integrate
ERC1155Nonfungible
into theopenzeppelin-contracts
library, I will be happy to develop this PR further, add tests, integrate feedback back into the code, etc. Thanks.