From 056c7cd6d820cae7b1a0336729aa8c26720ee016 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 1 Apr 2022 05:09:27 +0100 Subject: [PATCH] Implement ERC721CommonAutoIncrement to provide zero-indexed tokenId incrementing + totalSupply(). The _safeMintN(address,uint,[bytes]) functions work identically to _safeMint() but the uint specifies a number of tokens, not the next ID. --- .../erc721/ERC721CommonAutoIncrement.sol | 47 +++++++++++ .../erc721/TestableERC721CommonEnumerable.sol | 23 ++++++ tests/erc721/erc721_test.go | 79 +++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 contracts/erc721/ERC721CommonAutoIncrement.sol diff --git a/contracts/erc721/ERC721CommonAutoIncrement.sol b/contracts/erc721/ERC721CommonAutoIncrement.sol new file mode 100644 index 0000000..93be40b --- /dev/null +++ b/contracts/erc721/ERC721CommonAutoIncrement.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 the ethier authors (github.com/divergencetech/ethier) +pragma solidity >=0.8.0 <0.9.0; + +import "./ERC721Common.sol"; +import "../utils/Monotonic.sol"; + +/** +@notice An extension of the ethier ERC721Common contract that auto-increments +the next tokenId and exposes a totalSupply. + */ +contract ERC721CommonAutoIncrement is ERC721Common { + using Monotonic for Monotonic.Increaser; + + constructor(string memory name, string memory symbol) + ERC721Common(name, symbol) + {} + + /** + @notice Total number of tokens minted. + */ + Monotonic.Increaser public totalSupply; + + /** + @dev _safeMint() n tokens to the specified address, only incrementing + totalSupply once. + */ + function _safeMintN( + address to, + uint256 n, + bytes memory data + ) internal { + uint256 tokenId = totalSupply.current(); + uint256 end = tokenId + n; + for (; tokenId < end; ++tokenId) { + _safeMint(to, tokenId, data); + } + totalSupply.add(n); + } + + /** + @dev Alias for _safeMintN(address,uint,bytes) assuming an empty byte buffer. + */ + function _safeMintN(address to, uint256 n) internal { + _safeMintN(to, n, ""); + } +} diff --git a/tests/erc721/TestableERC721CommonEnumerable.sol b/tests/erc721/TestableERC721CommonEnumerable.sol index 0073289..0bae20a 100644 --- a/tests/erc721/TestableERC721CommonEnumerable.sol +++ b/tests/erc721/TestableERC721CommonEnumerable.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; import "../../contracts/erc721/ERC721CommonEnumerable.sol"; +import "../../contracts/erc721/ERC721AutoIncrement.sol"; import "../../contracts/erc721/BaseTokenURI.sol"; /// @notice Exposes a functions modified with the modifiers under test. @@ -38,3 +39,25 @@ contract TestableERC721CommonEnumerable is return BaseTokenURI._baseURI(); } } + +contract TestableERC721AutoIncrement is ERC721CommonAutoIncrement { + using Monotonic for Monotonic.Increaser; + + constructor() ERC721CommonAutoIncrement("", "") {} + + function safeMintN(address to, uint256 n) external { + _safeMintN(to, n); + } + + /** + @dev Returns all token owners for testing. + */ + function allOwners() external view returns (address[] memory) { + uint256 n = totalSupply.current(); + address[] memory owners = new address[](n); + for (uint256 i = 0; i < n; ++i) { + owners[i] = ownerOf(i); + } + return owners; + } +} diff --git a/tests/erc721/erc721_test.go b/tests/erc721/erc721_test.go index fd5f41c..4c0b3d9 100644 --- a/tests/erc721/erc721_test.go +++ b/tests/erc721/erc721_test.go @@ -19,6 +19,7 @@ import ( const ( deployer = iota tokenOwner + tokenOwner2 tokenReceiver approved vandal @@ -624,3 +625,81 @@ func TestBaseTokenURI(t *testing.T) { wantURI(t, 42, "good/42") wantURI(t, 101010, "good/101010") } + +func TestAutoIncrement(t *testing.T) { + sim := ethtest.NewSimulatedBackendTB(t, numAccounts) + openseatest.DeployProxyRegistryTB(t, sim) + + _, _, nft, err := DeployTestableERC721AutoIncrement(sim.Acc(deployer), sim) + if err != nil { + t.Fatalf("DeployTestableERC721AutoIncrement() error %v", err) + } + + const ( + o1 = tokenOwner + o2 = tokenOwner2 + ) + + tests := []struct { + toAccountIndex int + n int64 + wantOwnerIndices []int + wantTotalSupply int64 + }{ + { + toAccountIndex: o1, + n: 2, + wantOwnerIndices: []int{o1, o1}, + wantTotalSupply: 2, + }, + { + toAccountIndex: o2, + n: 0, + wantOwnerIndices: []int{o1, o1}, + wantTotalSupply: 2, + }, + { + toAccountIndex: o2, + n: 1, + wantOwnerIndices: []int{o1, o1, o2}, + wantTotalSupply: 3, + }, + { + toAccountIndex: o2, + n: 3, + wantOwnerIndices: []int{o1, o1, o2, o2, o2, o2}, + wantTotalSupply: 6, + }, + { + toAccountIndex: o1, + n: 1, + wantOwnerIndices: []int{o1, o1, o2, o2, o2, o2, o1}, + wantTotalSupply: 7, + }, + } + + for _, tt := range tests { + to := sim.Addr(tt.toAccountIndex) + sim.Must(t, "safeMintN(%v, %d)", to, tt.n)(nft.SafeMintN(sim.Acc(deployer), to, big.NewInt(tt.n))) + + t.Run("owners", func(t *testing.T) { + gotOwners, err := nft.AllOwners(nil) + if err != nil { + t.Fatalf("AllOwners() error %v", err) + } + + var wantOwners []common.Address + for _, i := range tt.wantOwnerIndices { + wantOwners = append(wantOwners, sim.Addr(i)) + } + + if diff := cmp.Diff(wantOwners, gotOwners); diff != "" { + t.Errorf("AllOwners() diff (-want +got):\n%s", diff) + } + }) + + if got, err := nft.TotalSupply(nil); err != nil || got.Cmp(big.NewInt(tt.wantTotalSupply)) != 0 { + t.Errorf("TotalSupply() got %d, err = %v; want %d, nil err", got, err, tt.wantTotalSupply) + } + } +}