Skip to content

Commit 167bf67

Browse files
Amxxfrangio
andcommitted
Fix ERC721Consecutive balance update on batch size 1
Merge pull request from GHSA-878m-3g6q-594q Co-authored-by: Francisco Giordano <[email protected]> (cherry picked from commit 8ba26f3)
1 parent 82d47ca commit 167bf67

File tree

5 files changed

+149
-12
lines changed

5 files changed

+149
-12
lines changed

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 4.8.2
4+
5+
-`ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`.
6+
7+
### Breaking changes
8+
9+
-`ERC721`: The internal function `_beforeTokenTransfer` no longer updates balances, which it previously did when `batchSize` was greater than 1. This change has no consequence unless a custom ERC721 extension is explicitly invoking `_beforeTokenTransfer`. Balance updates in extensions must now be done explicitly using `__unsafe_increaseBalance`, with a name that indicates that there is an invariant that has to be manually verified.
10+
311
## 4.8.1 (2023-01-13)
412

513
*`ERC4626`: Use staticcall instead of call when fetching underlying ERC-20 decimals. ([#3943](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3943))

‎contracts/token/ERC721/ERC721.sol‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -467,18 +467,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata{
467467
function _beforeTokenTransfer(
468468
addressfrom,
469469
addressto,
470-
uint256, /* firstTokenId */
470+
uint256firstTokenId,
471471
uint256batchSize
472-
) internalvirtual{
473-
if (batchSize >1){
474-
if (from !=address(0)){
475-
_balances[from] -= batchSize;
476-
}
477-
if (to !=address(0)){
478-
_balances[to] += batchSize;
479-
}
480-
}
481-
}
472+
) internalvirtual{}
482473

483474
/**
484475
* @dev Hook that is called after any token transfer. This includes minting and burning. If{ERC721Consecutive} is
@@ -500,4 +491,16 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata{
500491
uint256firstTokenId,
501492
uint256batchSize
502493
) internalvirtual{}
494+
495+
/**
496+
* @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an{ownerOf} override.
497+
*
498+
* WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant
499+
* being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such
500+
* that `ownerOf(tokenId)` is `a`.
501+
*/
502+
// solhint-disable-next-line func-name-mixedcase
503+
function __unsafe_increaseBalance(addressaccount, uint256amount) internal{
504+
_balances[account] += amount;
505+
}
503506
}

‎contracts/token/ERC721/extensions/ERC721Consecutive.sol‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ abstract contract ERC721Consecutive is IERC2309, ERC721{
9696
// push an ownership checkpoint & emit event
9797
uint96 last = first + batchSize -1;
9898
_sequentialOwnership.push(last, uint160(to));
99+
100+
// The invariant required by this function is preserved because the new sequentialOwnership checkpoint
101+
// is attributing ownership of `batchSize` new tokens to account `to`.
102+
__unsafe_increaseBalance(to, batchSize);
103+
99104
emitConsecutiveTransfer(first, last, address(0), to);
100105

101106
// hook after
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity^0.8.0;
4+
5+
import"../../../../contracts/token/ERC721/extensions/ERC721Consecutive.sol";
6+
import"forge-std/Test.sol";
7+
8+
function toSingleton(addressaccount) purereturns (address[] memory){
9+
address[] memory accounts =newaddress[](1);
10+
accounts[0] = account;
11+
return accounts;
12+
}
13+
14+
contractERC721ConsecutiveTargetisStdUtils, ERC721Consecutive{
15+
uint256public totalMinted =0;
16+
17+
constructor(address[] memoryreceivers, uint256[] memorybatches) ERC721("", ""){
18+
for (uint256 i =0; i < batches.length; i++){
19+
address receiver = receivers[i % receivers.length];
20+
uint96 batchSize =uint96(bound(batches[i], 0, _maxBatchSize()));
21+
_mintConsecutive(receiver, batchSize);
22+
totalMinted += batchSize;
23+
}
24+
}
25+
26+
function burn(uint256tokenId) public{
27+
_burn(tokenId);
28+
}
29+
}
30+
31+
contractERC721ConsecutiveTestisTest{
32+
function test_balance(addressreceiver, uint256[] calldatabatches) public{
33+
vm.assume(receiver !=address(0));
34+
35+
ERC721ConsecutiveTarget token =newERC721ConsecutiveTarget(toSingleton(receiver), batches);
36+
37+
assertEq(token.balanceOf(receiver), token.totalMinted());
38+
}
39+
40+
function test_ownership(addressreceiver, uint256[] calldatabatches, uint256[2] calldataunboundedTokenId) public{
41+
vm.assume(receiver !=address(0));
42+
43+
ERC721ConsecutiveTarget token =newERC721ConsecutiveTarget(toSingleton(receiver), batches);
44+
45+
if (token.totalMinted() >0){
46+
uint256 validTokenId =bound(unboundedTokenId[0], 0, token.totalMinted() -1);
47+
assertEq(token.ownerOf(validTokenId), receiver);
48+
}
49+
50+
uint256 invalidTokenId =bound(unboundedTokenId[1], token.totalMinted(), type(uint256).max);
51+
vm.expectRevert();
52+
token.ownerOf(invalidTokenId);
53+
}
54+
55+
function test_burn(addressreceiver, uint256[] calldatabatches, uint256unboundedTokenId) public{
56+
vm.assume(receiver !=address(0));
57+
58+
ERC721ConsecutiveTarget token =newERC721ConsecutiveTarget(toSingleton(receiver), batches);
59+
60+
// only test if we minted at least one token
61+
uint256 supply = token.totalMinted();
62+
vm.assume(supply >0);
63+
64+
// burn a token in [0; supply[
65+
uint256 tokenId =bound(unboundedTokenId, 0, supply -1);
66+
token.burn(tokenId);
67+
68+
// balance should have decreased
69+
assertEq(token.balanceOf(receiver), supply -1);
70+
71+
// token should be burnt
72+
vm.expectRevert();
73+
token.ownerOf(tokenId);
74+
}
75+
76+
function test_transfer(
77+
address[2] calldataaccounts,
78+
uint256[2] calldataunboundedBatches,
79+
uint256[2] calldataunboundedTokenId
80+
) public{
81+
vm.assume(accounts[0] !=address(0));
82+
vm.assume(accounts[1] !=address(0));
83+
vm.assume(accounts[0] != accounts[1]);
84+
85+
address[] memory receivers =newaddress[](2);
86+
receivers[0] = accounts[0];
87+
receivers[1] = accounts[1];
88+
89+
// We assume _maxBatchSize is 5000 (the default). This test will break otherwise.
90+
uint256[] memory batches =newuint256[](2);
91+
batches[0] =bound(unboundedBatches[0], 1, 5000);
92+
batches[1] =bound(unboundedBatches[1], 1, 5000);
93+
94+
ERC721ConsecutiveTarget token =newERC721ConsecutiveTarget(receivers, batches);
95+
96+
uint256 tokenId0 =bound(unboundedTokenId[0], 0, batches[0] -1);
97+
uint256 tokenId1 =bound(unboundedTokenId[1], 0, batches[1] -1) + batches[0];
98+
99+
assertEq(token.ownerOf(tokenId0), accounts[0]);
100+
assertEq(token.ownerOf(tokenId1), accounts[1]);
101+
assertEq(token.balanceOf(accounts[0]), batches[0]);
102+
assertEq(token.balanceOf(accounts[1]), batches[1]);
103+
104+
vm.prank(accounts[0]);
105+
token.transferFrom(accounts[0], accounts[1], tokenId0);
106+
107+
assertEq(token.ownerOf(tokenId0), accounts[1]);
108+
assertEq(token.ownerOf(tokenId1), accounts[1]);
109+
assertEq(token.balanceOf(accounts[0]), batches[0] -1);
110+
assertEq(token.balanceOf(accounts[1]), batches[1] +1);
111+
112+
vm.prank(accounts[1]);
113+
token.transferFrom(accounts[1], accounts[0], tokenId1);
114+
115+
assertEq(token.ownerOf(tokenId0), accounts[1]);
116+
assertEq(token.ownerOf(tokenId1), accounts[0]);
117+
assertEq(token.balanceOf(accounts[0]), batches[0]);
118+
assertEq(token.balanceOf(accounts[1]), batches[1]);
119+
}
120+
}

‎test/token/ERC721/extensions/ERC721Consecutive.test.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ contract('ERC721Consecutive', function (accounts){
1212
constsymbol='NFT';
1313
constbatches=[
1414
{receiver: user1,amount: 0},
15-
{receiver: user1,amount: 3},
15+
{receiver: user1,amount: 1},
16+
{receiver: user1,amount: 2},
1617
{receiver: user2,amount: 5},
1718
{receiver: user3,amount: 0},
1819
{receiver: user1,amount: 7},

0 commit comments

Comments
(0)