Skip to content

Commit 4c713f8

Browse files
committed
Merge branch 'master' into next-v5.0
2 parents 7bb5592 + 5420879 commit 4c713f8

File tree

5 files changed

+235
-140
lines changed

5 files changed

+235
-140
lines changed

.changeset/spotty-hotels-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': patch
3+
---
4+
5+
`ERC721Consecutive`: Add a `_firstConsecutiveId` internal function that can be overridden to change the id of the first token minted through `_mintConsecutive`.

contracts/mocks/token/ERC721ConsecutiveMock.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import "../../token/ERC721/extensions/ERC721Votes.sol";
1111
* @title ERC721ConsecutiveMock
1212
*/
1313
contract ERC721ConsecutiveMock is ERC721Consecutive, ERC721Pausable, ERC721Votes {
14+
uint96 private immutable _offset;
15+
1416
constructor(
1517
string memory name,
1618
string memory symbol,
19+
uint96 offset,
1720
address[] memory delegates,
1821
address[] memory receivers,
1922
uint96[] memory amounts
2023
) ERC721(name, symbol) EIP712(name, "1") {
24+
_offset = offset;
25+
2126
for (uint256 i = 0; i < delegates.length; ++i) {
2227
_delegate(delegates[i], delegates[i]);
2328
}
@@ -27,6 +32,10 @@ contract ERC721ConsecutiveMock is ERC721Consecutive, ERC721Pausable, ERC721Votes
2732
}
2833
}
2934

35+
function _firstConsecutiveId() internal view virtual override returns (uint96) {
36+
return _offset;
37+
}
38+
3039
function _ownerOf(uint256 tokenId) internal view virtual override(ERC721, ERC721Consecutive) returns (address) {
3140
return super._ownerOf(tokenId);
3241
}

contracts/token/ERC721/extensions/ERC721Consecutive.sol

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import "../../../utils/structs/Checkpoints.sol";
2020
* regained after construction. During construction, only batch minting is allowed.
2121
*
2222
* IMPORTANT: This extension bypasses the hooks {_beforeTokenTransfer} and {_afterTokenTransfer} for tokens minted in
23-
* batch. When using this extension, you should consider the {_beforeConsecutiveTokenTransfer} and
24-
* {_afterConsecutiveTokenTransfer} hooks in addition to {_beforeTokenTransfer} and {_afterTokenTransfer}.
23+
* batch. The hooks will be only called once per batch, so you should take `batchSize` parameter into consideration
24+
* when relying on hooks.
2525
*
2626
* IMPORTANT: When overriding {_afterTokenTransfer}, be careful about call ordering. {ownerOf} may return invalid
2727
* values during the {_afterTokenTransfer} execution if the super call is not called first. To be safe, execute the
@@ -56,7 +56,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 {
5656
address owner = super._ownerOf(tokenId);
5757

5858
// If token is owned by the core, or beyond consecutive range, return base value
59-
if (owner != address(0) || tokenId > type(uint96).max) {
59+
if (owner != address(0) || tokenId > type(uint96).max || tokenId < _firstConsecutiveId()) {
6060
return owner;
6161
}
6262

@@ -82,7 +82,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 {
8282
* Emits a {IERC2309-ConsecutiveTransfer} event.
8383
*/
8484
function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) {
85-
uint96 first = _totalConsecutiveSupply();
85+
uint96 next = _nextConsecutiveId();
8686

8787
// minting a batch of size 0 is a no-op
8888
if (batchSize > 0) {
@@ -91,29 +91,29 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 {
9191
require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large");
9292

9393
// hook before
94-
_beforeTokenTransfer(address(0), to, first, batchSize);
94+
_beforeTokenTransfer(address(0), to, next, batchSize);
9595

9696
// push an ownership checkpoint & emit event
97-
uint96 last = first + batchSize - 1;
97+
uint96 last = next + batchSize - 1;
9898
_sequentialOwnership.push(last, uint160(to));
9999

100100
// The invariant required by this function is preserved because the new sequentialOwnership checkpoint
101101
// is attributing ownership of `batchSize` new tokens to account `to`.
102102
__unsafe_increaseBalance(to, batchSize);
103103

104-
emit ConsecutiveTransfer(first, last, address(0), to);
104+
emit ConsecutiveTransfer(next, last, address(0), to);
105105

106106
// hook after
107-
_afterTokenTransfer(address(0), to, first, batchSize);
107+
_afterTokenTransfer(address(0), to, next, batchSize);
108108
}
109109

110-
return first;
110+
return next;
111111
}
112112

113113
/**
114114
* @dev See {ERC721-_mint}. Override version that restricts normal minting to after construction.
115115
*
116-
* Warning: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}.
116+
* WARNING: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}.
117117
* After construction, {_mintConsecutive} is no longer available and {_mint} becomes available.
118118
*/
119119
function _mint(address to, uint256 tokenId) internal virtual override {
@@ -132,17 +132,30 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 {
132132
) internal virtual override {
133133
if (
134134
to == address(0) && // if we burn
135-
firstTokenId < _totalConsecutiveSupply() && // and the tokenId was minted in a batch
136-
!_sequentialBurn.get(firstTokenId) // and the token was never marked as burnt
137-
) {
135+
firstTokenId >= _firstConsecutiveId() &&
136+
firstTokenId < _nextConsecutiveId() &&
137+
!_sequentialBurn.get(firstTokenId)
138+
) // and the token was never marked as burnt
139+
{
138140
require(batchSize == 1, "ERC721Consecutive: batch burn not supported");
139141
_sequentialBurn.set(firstTokenId);
140142
}
141143
super._afterTokenTransfer(from, to, firstTokenId, batchSize);
142144
}
143145

144-
function _totalConsecutiveSupply() private view returns (uint96) {
146+
/**
147+
* @dev Used to offset the first token id in {_nextConsecutiveId}
148+
*/
149+
function _firstConsecutiveId() internal view virtual returns (uint96) {
150+
return 0;
151+
}
152+
153+
/**
154+
* @dev Returns the next tokenId to mint using {_mintConsecutive}. It will return {_firstConsecutiveId}
155+
* if no consecutive tokenId has been minted before.
156+
*/
157+
function _nextConsecutiveId() private view returns (uint96) {
145158
(bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint();
146-
return exists ? latestId + 1 : 0;
159+
return exists ? latestId + 1 : _firstConsecutiveId();
147160
}
148161
}

test/token/ERC721/extensions/ERC721Consecutive.t.sol

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ function toSingleton(address account) pure returns (address[] memory) {
1414
}
1515

1616
contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive {
17+
uint96 private immutable _offset;
1718
uint256 public totalMinted = 0;
1819

19-
constructor(address[] memory receivers, uint256[] memory batches) ERC721("", "") {
20+
constructor(address[] memory receivers, uint256[] memory batches, uint256 startingId) ERC721("", "") {
21+
_offset = uint96(startingId);
2022
for (uint256 i = 0; i < batches.length; i++) {
2123
address receiver = receivers[i % receivers.length];
2224
uint96 batchSize = uint96(bound(batches[i], 0, _maxBatchSize()));
@@ -28,43 +30,71 @@ contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive {
2830
function burn(uint256 tokenId) public {
2931
_burn(tokenId);
3032
}
33+
34+
function _firstConsecutiveId() internal view virtual override returns (uint96) {
35+
return _offset;
36+
}
3137
}
3238

3339
contract ERC721ConsecutiveTest is Test {
34-
function test_balance(address receiver, uint256[] calldata batches) public {
40+
function test_balance(address receiver, uint256[] calldata batches, uint96 startingId) public {
3541
vm.assume(receiver != address(0));
3642

37-
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches);
43+
uint256 startingTokenId = bound(startingId, 0, 5000);
44+
45+
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId);
3846

3947
assertEq(token.balanceOf(receiver), token.totalMinted());
4048
}
4149

42-
function test_ownership(address receiver, uint256[] calldata batches, uint256[2] calldata unboundedTokenId) public {
50+
function test_ownership(
51+
address receiver,
52+
uint256[] calldata batches,
53+
uint256[2] calldata unboundedTokenId,
54+
uint96 startingId
55+
) public {
4356
vm.assume(receiver != address(0));
4457

45-
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches);
58+
uint256 startingTokenId = bound(startingId, 0, 5000);
59+
60+
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId);
4661

4762
if (token.totalMinted() > 0) {
48-
uint256 validTokenId = bound(unboundedTokenId[0], 0, token.totalMinted() - 1);
63+
uint256 validTokenId = bound(
64+
unboundedTokenId[0],
65+
startingTokenId,
66+
startingTokenId + token.totalMinted() - 1
67+
);
4968
assertEq(token.ownerOf(validTokenId), receiver);
5069
}
5170

52-
uint256 invalidTokenId = bound(unboundedTokenId[1], token.totalMinted(), type(uint256).max);
71+
uint256 invalidTokenId = bound(
72+
unboundedTokenId[1],
73+
startingTokenId + token.totalMinted(),
74+
startingTokenId + token.totalMinted() + 1
75+
);
5376
vm.expectRevert();
5477
token.ownerOf(invalidTokenId);
5578
}
5679

57-
function test_burn(address receiver, uint256[] calldata batches, uint256 unboundedTokenId) public {
80+
function test_burn(
81+
address receiver,
82+
uint256[] calldata batches,
83+
uint256 unboundedTokenId,
84+
uint96 startingId
85+
) public {
5886
vm.assume(receiver != address(0));
5987

60-
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches);
88+
uint256 startingTokenId = bound(startingId, 0, 5000);
89+
90+
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId);
6191

6292
// only test if we minted at least one token
6393
uint256 supply = token.totalMinted();
6494
vm.assume(supply > 0);
6595

6696
// burn a token in [0; supply[
67-
uint256 tokenId = bound(unboundedTokenId, 0, supply - 1);
97+
uint256 tokenId = bound(unboundedTokenId, startingTokenId, startingTokenId + supply - 1);
6898
token.burn(tokenId);
6999

70100
// balance should have decreased
@@ -78,25 +108,28 @@ contract ERC721ConsecutiveTest is Test {
78108
function test_transfer(
79109
address[2] calldata accounts,
80110
uint256[2] calldata unboundedBatches,
81-
uint256[2] calldata unboundedTokenId
111+
uint256[2] calldata unboundedTokenId,
112+
uint96 startingId
82113
) public {
83114
vm.assume(accounts[0] != address(0));
84115
vm.assume(accounts[1] != address(0));
85116
vm.assume(accounts[0] != accounts[1]);
86117

118+
uint256 startingTokenId = bound(startingId, 1, 5000);
119+
87120
address[] memory receivers = new address[](2);
88121
receivers[0] = accounts[0];
89122
receivers[1] = accounts[1];
90123

91124
// We assume _maxBatchSize is 5000 (the default). This test will break otherwise.
92125
uint256[] memory batches = new uint256[](2);
93-
batches[0] = bound(unboundedBatches[0], 1, 5000);
94-
batches[1] = bound(unboundedBatches[1], 1, 5000);
126+
batches[0] = bound(unboundedBatches[0], startingTokenId, 5000);
127+
batches[1] = bound(unboundedBatches[1], startingTokenId, 5000);
95128

96-
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches);
129+
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches, startingTokenId);
97130

98-
uint256 tokenId0 = bound(unboundedTokenId[0], 0, batches[0] - 1);
99-
uint256 tokenId1 = bound(unboundedTokenId[1], 0, batches[1] - 1) + batches[0];
131+
uint256 tokenId0 = bound(unboundedTokenId[0], startingTokenId, batches[0]);
132+
uint256 tokenId1 = bound(unboundedTokenId[1], startingTokenId, batches[1]) + batches[0];
100133

101134
assertEq(token.ownerOf(tokenId0), accounts[0]);
102135
assertEq(token.ownerOf(tokenId1), accounts[1]);
@@ -119,4 +152,29 @@ contract ERC721ConsecutiveTest is Test {
119152
assertEq(token.balanceOf(accounts[0]), batches[0]);
120153
assertEq(token.balanceOf(accounts[1]), batches[1]);
121154
}
155+
156+
function test_start_consecutive_id(
157+
address receiver,
158+
uint256[2] calldata unboundedBatches,
159+
uint256[2] calldata unboundedTokenId,
160+
uint96 startingId
161+
) public {
162+
vm.assume(receiver != address(0));
163+
164+
uint256 startingTokenId = bound(startingId, 1, 5000);
165+
166+
// We assume _maxBatchSize is 5000 (the default). This test will break otherwise.
167+
uint256[] memory batches = new uint256[](2);
168+
batches[0] = bound(unboundedBatches[0], startingTokenId, 5000);
169+
batches[1] = bound(unboundedBatches[1], startingTokenId, 5000);
170+
171+
ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId);
172+
173+
uint256 tokenId0 = bound(unboundedTokenId[0], startingTokenId, batches[0]);
174+
uint256 tokenId1 = bound(unboundedTokenId[1], startingTokenId, batches[1]);
175+
176+
assertEq(token.ownerOf(tokenId0), receiver);
177+
assertEq(token.ownerOf(tokenId1), receiver);
178+
assertEq(token.balanceOf(receiver), batches[0] + batches[1]);
179+
}
122180
}

0 commit comments

Comments
 (0)