Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nonfungible ERC1155 implementation #4684

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dwardu
Copy link
Contributor

@dwardu dwardu commented Oct 13, 2023

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:

abstract contract ERC1155NonfungibleNaive is ERC1155Supply {
    function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual override {
        super._update(from, to, ids, values);
        for (uint256 i = 0; i < ids.length; ++i) {
            if (values[i] > 1) revert ERC1155NonfungibleInvalidValue(ids[i], values[i]);
            uint256 supply = totalSupply(ids[i]);
            if (supply > 1) revert ERC1155NonfungibleIllegalSupply(ids[i], supply);
        }
    }
}

However this is gas-inefficient because on every transfer, not only is the contract internally modifying both_balances[id][from] and _balances[id][to] (in ERC1155 grandparent), but the contract also needs to track the _totalSupply[id] (in ERC1155Supply 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 any totalSupplyAll-related code from the ERC1155Supply parent. Let’s call this implementation ERC1155NonfungibleNaiveLite.

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, named ERC1155Nonfungible, that is written as an extension of openzeppelin-contract’s ERC1155. Like ERC1155D, ERC1155Nonfungible also cuts down gas usage significantly by tracking ownership instead of balances, but ERC1155Nonfungible focuses less on squeezing out up to the last drop of gas, and more on having a more concise implementation that fits better into this openzeppelin-contracts library.

To evaluate ERC1155Nonfungible, we create a simple test where we mint a token to account1, then transfer it from account1 to account2, and then burn the token. The test was run with all the different ERC-1155 implementations mentioned above, and also with the ERC721 contract, and the following gas usage was recorded:

Contract mint transfer burn
ERC721 72119 58301 29215
ERC1155NonfungibleNaive 96580 54818 33434
ERC1155NonfungibleNaiveLite 74380 54818 29419
ERC1155Nonfungible 51337 34749 26095
ERC1155D 50810 34430 25775

ERC1155Nonfungible and ERC1155D 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 than ERC1155Nonfungible, as ERC1155Nonfungible sacrifices some gas optimization, but in exchange for being ~5 times more compact and fitting better into the openzeppelin-contracts library architecture.


As far as the ERC1155Nonfungible implementation is concerned, some arbitrary decisions were taken:

  1. Token ownership is read/written exclusively through overridable getter/setter function. This makes the implementation slighlty less compact, but it makes it possible to extend 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)
  2. ownerOf() is exposed externally, and it returns zero-address for a non-existent token, rather than reverting (as in ERC-721)
  3. An attempt to mint an existent token will revert with a custom error ERC1155NonfungibleDuplicate. If we are to follow ERC721’s lead, we would flag this error condition by reverting with ERC1155InvalidSender(address(0)), but it seems more clear to create a dedicated error.

If there is interest to integrate ERC1155Nonfungible into the openzeppelin-contracts library, I will be happy to develop this PR further, add tests, integrate feedback back into the code, etc. Thanks.

@changeset-bot
Copy link

changeset-bot bot commented Oct 13, 2023

⚠️ No Changeset found

Latest commit: 954ebe6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dwardu dwardu force-pushed the feature/erc1155-nonfungible branch from 48d09ce to 23f739b Compare October 13, 2023 23:37
@ernestognw ernestognw added this to the 5.x milestone Oct 16, 2023
@ernestognw
Copy link
Member

ernestognw commented Oct 16, 2023

Hey @dwardu, thanks for the PR, I think it's an interesting proposal and something worth considering.

Recently we released 5.0 so we're deciding on what items should go next in the roadmap. Although this proposal makes sense, I'd like to discuss it first and then come back before developing further.

The analysis you've provided makes sense (including the gas benchmarks), but I'd like to see what projects are already using this ERC1155 variant just to confirm demand, can you share some examples? (if any)

On a side note, we'd also like to consider if ERC1155 is the best fit if gas optimization is the purpose. Perhaps an alternative could be a non-fungible ERC-6909.

@dwardu
Copy link
Contributor Author

dwardu commented Oct 16, 2023

Thanks @ernestognw.

First major contract that comes to mind that uses ERC-1155 for nonfungible tokens is the ENS NameWrapper.

On OpenSea, if never more than 1 of a token-id ever existed on a contract, the contract seems to be treated as an nonfungible token contract (e.g. under token transfers the amount is omitted rather than being shown as “1”). This is not a use of ERC-1155 for NFTs per se, but rather what appears to be support from a major platform for nonfungible ERC-1155 tokens.

I’ll see if can compile some stats using The Graph or Etherscan API.

ERC-6909 may well turn out to be a popular standard in the future, whereas ERC-1155 is supported by Etherscan, OpenSea and other major tools and applications right now.

@dwardu
Copy link
Contributor Author

dwardu commented Oct 22, 2023

Hey @ernestognw, I have compiled a list of ERC-1155 mainnet contracts being used for nonfungible tokens.

The ENS NameWrapper contract seems to be the ERC-1155 nonfungible token contract with the largest token supply, but there are also many others. Inspecting some of the top contracts, some (e.g. this and this) seem to be explicitly using the contract for nonfungibile tokens (mint amount of 1 is hardcoded), while (many) others are able to handle amounts ≥ 1, but still seem to be being used exclusively for amounts of 1 (which is why they made it to the the list.)

@dwardu dwardu force-pushed the feature/erc1155-nonfungible branch from 23f739b to e204a77 Compare October 22, 2023 22:28
@dwardu dwardu force-pushed the feature/erc1155-nonfungible branch from e204a77 to 954ebe6 Compare October 31, 2023 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants