Skip to content

Commit fb22621

Browse files
Vectorizedahbanavi
andauthored
Added burn functionality (chiru-labs#61)
* Added burn functionality. * Changed _initOneIndexed * Moved burn function into ERC721ABurnable * Moved burn function into ERC721ABurnable * Remove redundant burn check in ownershipOf * Optimized ownershipOf * Removed aux from AddressData for future PR * Packed currentIndex and totalBurned for gas savings * Added gas optimizations * Added gas optimizations * Added requested changes * Edited comments. * Renamed totalBurned to burnedCounter * Renamed to burnCounter * Updated comments. * Mark transferFrom/safeTransferFrom virtual * Mark transferFrom/safeTransferFrom virtual * Updated comments. * Tidy up tests * Inlined _exists for _burn and _transfer. * Merged custom errors * Merged custom errors * Fixed missing change from chiru-labs#59 * Gas optimization * update specs for _beforeTokenTransfers and _afterTokenTransfers hooks * Added chiru-labs#84 * Added chiru-labs#87 * Added chiru-labs#85 * Added chiru-labs#89 * Added comments on packing _currentIndex and _burnCounter * Removed overflow check for updatedIndex * Added requested test changes * Removed unused variable in burn test * Removed unused variable in burn test Co-authored-by: Amirhossein Banavi <ahbanavi@gmail.com>
1 parent d886d4d commit fb22621

9 files changed

+444
-36
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ contract Azuki is ERC721A {
5454

5555
## Roadmap
5656

57-
- [] Add burn function
5857
- [] Add flexibility for the first token id to not start at 0
5958
- [] Support ERC721 Upgradeable
6059
- [] Add more documentation on benefits of using ERC721A

contracts/ERC721A.sol

+157-32
Large diffs are not rendered by default.
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creator: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
import '../ERC721A.sol';
7+
import '@openzeppelin/contracts/utils/Context.sol';
8+
9+
/**
10+
* @title ERC721A Burnable Token
11+
* @dev ERC721A Token that can be irreversibly burned (destroyed).
12+
*/
13+
abstract contract ERC721ABurnable is Context, ERC721A {
14+
15+
/**
16+
* @dev Burns `tokenId`. See {ERC721A-_burn}.
17+
*
18+
* Requirements:
19+
*
20+
* - The caller must own `tokenId` or be an approved operator.
21+
*/
22+
function burn(uint256 tokenId) public virtual {
23+
TokenOwnership memory prevOwnership = ownershipOf(tokenId);
24+
25+
bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr ||
26+
isApprovedForAll(prevOwnership.addr, _msgSender()) ||
27+
getApproved(tokenId) == _msgSender());
28+
29+
if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved();
30+
31+
_burn(tokenId);
32+
}
33+
}

contracts/extensions/ERC721AOwnersExplicit.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
3232
}
3333

3434
for (uint256 i = _nextOwnerToExplicitlySet; i <= endIndex; i++) {
35-
if (_ownerships[i].addr == address(0)) {
35+
if (_ownerships[i].addr == address(0) && !_ownerships[i].burned) {
3636
TokenOwnership memory ownership = ownershipOf(i);
3737
_ownerships[i].addr = ownership.addr;
3838
_ownerships[i].startTimestamp = ownership.startTimestamp;
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creators: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
import '../extensions/ERC721ABurnable.sol';
7+
8+
contract ERC721ABurnableMock is ERC721A, ERC721ABurnable {
9+
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}
10+
11+
function exists(uint256 tokenId) public view returns (bool) {
12+
return _exists(tokenId);
13+
}
14+
15+
function safeMint(address to, uint256 quantity) public {
16+
_safeMint(to, quantity);
17+
}
18+
19+
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
20+
return _ownerships[index];
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creators: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
import '../extensions/ERC721ABurnable.sol';
7+
import '../extensions/ERC721AOwnersExplicit.sol';
8+
9+
contract ERC721ABurnableOwnersExplicitMock is ERC721A, ERC721ABurnable, ERC721AOwnersExplicit {
10+
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}
11+
12+
function exists(uint256 tokenId) public view returns (bool) {
13+
return _exists(tokenId);
14+
}
15+
16+
function safeMint(address to, uint256 quantity) public {
17+
_safeMint(to, quantity);
18+
}
19+
20+
function setOwnersExplicit(uint256 quantity) public {
21+
_setOwnersExplicit(quantity);
22+
}
23+
24+
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
25+
return _ownerships[index];
26+
}
27+
}

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
const { expect } = require('chai');
2+
3+
describe('ERC721ABurnable', function () {
4+
5+
beforeEach(async function () {
6+
this.ERC721ABurnable = await ethers.getContractFactory('ERC721ABurnableMock');
7+
this.token = await this.ERC721ABurnable.deploy('Azuki', 'AZUKI');
8+
await this.token.deployed();
9+
});
10+
11+
beforeEach(async function () {
12+
const [owner, addr1, addr2] = await ethers.getSigners();
13+
this.owner = owner;
14+
this.addr1 = addr1;
15+
this.addr2 = addr2;
16+
this.numTestTokens = 10;
17+
this.burnedTokenId = 5;
18+
await this.token['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens);
19+
await this.token.connect(this.addr1).burn(this.burnedTokenId);
20+
});
21+
22+
it('changes exists', async function () {
23+
expect(await this.token.exists(this.burnedTokenId)).to.be.false;
24+
});
25+
26+
it('cannot burn a non-existing token', async function () {
27+
const query = this.token.connect(this.addr1).burn(this.numTestTokens);
28+
await expect(query).to.be.revertedWith(
29+
'OwnerQueryForNonexistentToken'
30+
);
31+
});
32+
33+
it('cannot burn a burned token', async function () {
34+
const query = this.token.connect(this.addr1).burn(this.burnedTokenId);
35+
await expect(query).to.be.revertedWith(
36+
'OwnerQueryForNonexistentToken'
37+
);
38+
})
39+
40+
it('cannot transfer a burned token', async function () {
41+
const query = this.token.connect(this.addr1)
42+
.transferFrom(this.addr1.address, this.addr2.address, this.burnedTokenId);
43+
await expect(query).to.be.revertedWith(
44+
'OwnerQueryForNonexistentToken'
45+
);
46+
})
47+
48+
it('reduces totalSupply', async function () {
49+
const supplyBefore = await this.token.totalSupply();
50+
for (let i = 0; i < 2; ++i) {
51+
await this.token.connect(this.addr1).burn(i);
52+
expect(supplyBefore - (await this.token.totalSupply())).to.equal(i + 1);
53+
}
54+
})
55+
56+
it('adjusts owners tokens by index', async function () {
57+
const n = await this.token.totalSupply();
58+
for (let i = 0; i < this.burnedTokenId; ++i) {
59+
expect(await this.token.tokenByIndex(i)).to.be.equal(i);
60+
}
61+
for (let i = this.burnedTokenId; i < n; ++i) {
62+
expect(await this.token.tokenByIndex(i)).to.be.equal(i + 1);
63+
}
64+
// tokenIds of addr1: [0,1,2,3,4,6,7,8,9]
65+
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
66+
.to.be.equal(2);
67+
await this.token.connect(this.addr1).burn(2);
68+
// tokenIds of addr1: [0,1,3,4,6,7,8,9]
69+
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
70+
.to.be.equal(3);
71+
await this.token.connect(this.addr1).burn(0);
72+
// tokenIds of addr1: [1,3,4,6,7,8,9]
73+
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
74+
.to.be.equal(4);
75+
await this.token.connect(this.addr1).burn(3);
76+
// tokenIds of addr1: [1,4,6,7,8,9]
77+
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
78+
.to.be.equal(6);
79+
})
80+
81+
it('adjusts owners balances', async function () {
82+
expect(await this.token.balanceOf(this.addr1.address))
83+
.to.be.equal(this.numTestTokens - 1);
84+
});
85+
86+
it('adjusts token by index', async function () {
87+
const n = await this.token.totalSupply();
88+
for (let i = 0; i < this.burnedTokenId; ++i) {
89+
expect(await this.token.tokenByIndex(i)).to.be.equal(i);
90+
}
91+
for (let i = this.burnedTokenId; i < n; ++i) {
92+
expect(await this.token.tokenByIndex(i)).to.be.equal(i + 1);
93+
}
94+
await expect(this.token.tokenByIndex(n)).to.be.revertedWith(
95+
'TokenIndexOutOfBounds'
96+
);
97+
});
98+
99+
describe('ownerships correctly set', async function () {
100+
it('with token before previously burnt token transfered and burned', async function () {
101+
const tokenIdToBurn = this.burnedTokenId - 1;
102+
await this.token.connect(this.addr1)
103+
.transferFrom(this.addr1.address, this.addr2.address, tokenIdToBurn);
104+
expect(await this.token.ownerOf(tokenIdToBurn)).to.be.equal(this.addr2.address);
105+
await this.token.connect(this.addr2).burn(tokenIdToBurn);
106+
for (let i = 0; i < this.numTestTokens; ++i) {
107+
if (i == tokenIdToBurn || i == this.burnedTokenId) {
108+
await expect(this.token.ownerOf(i)).to.be.revertedWith(
109+
'OwnerQueryForNonexistentToken'
110+
);
111+
} else {
112+
expect(await this.token.ownerOf(i)).to.be.equal(this.addr1.address);
113+
}
114+
}
115+
});
116+
117+
it('with token after previously burnt token transfered and burned', async function () {
118+
const tokenIdToBurn = this.burnedTokenId + 1;
119+
await this.token.connect(this.addr1)
120+
.transferFrom(this.addr1.address, this.addr2.address, tokenIdToBurn);
121+
expect(await this.token.ownerOf(tokenIdToBurn)).to.be.equal(this.addr2.address);
122+
await this.token.connect(this.addr2).burn(tokenIdToBurn);
123+
for (let i = 0; i < this.numTestTokens; ++i) {
124+
if (i == tokenIdToBurn || i == this.burnedTokenId) {
125+
await expect(this.token.ownerOf(i)).to.be.revertedWith(
126+
'OwnerQueryForNonexistentToken'
127+
)
128+
} else {
129+
expect(await this.token.ownerOf(i)).to.be.equal(this.addr1.address);
130+
}
131+
}
132+
});
133+
134+
it('with first token burned', async function () {
135+
await this.token.connect(this.addr1).burn(0);
136+
for (let i = 0; i < this.numTestTokens; ++i) {
137+
if (i == 0 || i == this.burnedTokenId) {
138+
await expect(this.token.ownerOf(i)).to.be.revertedWith(
139+
'OwnerQueryForNonexistentToken'
140+
)
141+
} else {
142+
expect(await this.token.ownerOf(i)).to.be.equal(this.addr1.address);
143+
}
144+
}
145+
});
146+
147+
it('with last token burned', async function () {
148+
await expect(this.token.ownerOf(this.numTestTokens)).to.be.revertedWith(
149+
'OwnerQueryForNonexistentToken'
150+
)
151+
await this.token.connect(this.addr1).burn(this.numTestTokens - 1);
152+
await expect(this.token.ownerOf(this.numTestTokens - 1)).to.be.revertedWith(
153+
'OwnerQueryForNonexistentToken'
154+
)
155+
});
156+
});
157+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const { expect } = require('chai');
2+
const { constants } = require('@openzeppelin/test-helpers');
3+
const { ZERO_ADDRESS } = constants;
4+
5+
describe('ERC721ABurnableOwnersExplicit', function () {
6+
beforeEach(async function () {
7+
this.ERC721ABurnableOwnersExplicit = await ethers.getContractFactory('ERC721ABurnableOwnersExplicitMock');
8+
this.token = await this.ERC721ABurnableOwnersExplicit.deploy('Azuki', 'AZUKI');
9+
await this.token.deployed();
10+
});
11+
12+
beforeEach(async function () {
13+
const [owner, addr1, addr2, addr3] = await ethers.getSigners();
14+
this.owner = owner;
15+
this.addr1 = addr1;
16+
this.addr2 = addr2;
17+
this.addr3 = addr3;
18+
await this.token['safeMint(address,uint256)'](addr1.address, 1);
19+
await this.token['safeMint(address,uint256)'](addr2.address, 2);
20+
await this.token['safeMint(address,uint256)'](addr3.address, 3);
21+
await this.token.connect(this.addr1).burn(0);
22+
await this.token.connect(this.addr3).burn(4);
23+
await this.token.setOwnersExplicit(6);
24+
});
25+
26+
it('ownerships correctly set', async function () {
27+
for (let tokenId = 0; tokenId < 6; tokenId++) {
28+
let owner = await this.token.getOwnershipAt(tokenId);
29+
expect(owner[0]).to.not.equal(ZERO_ADDRESS);
30+
if (tokenId == 0 || tokenId == 4) {
31+
expect(owner[2]).to.equal(true);
32+
await expect(this.token.ownerOf(tokenId)).to.be.revertedWith(
33+
'OwnerQueryForNonexistentToken'
34+
)
35+
} else {
36+
expect(owner[2]).to.equal(false);
37+
if (tokenId < 1+2) {
38+
expect(await this.token.ownerOf(tokenId)).to.be.equal(this.addr2.address);
39+
} else {
40+
expect(await this.token.ownerOf(tokenId)).to.be.equal(this.addr3.address);
41+
}
42+
}
43+
}
44+
});
45+
});

0 commit comments

Comments
 (0)