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

Added burn functionality #61

Merged
merged 41 commits into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d92c3d0
Added burn functionality.
Vectorized Feb 1, 2022
4a8bc00
Changed _initOneIndexed
Vectorized Feb 1, 2022
00fe576
Moved burn function into ERC721ABurnable
Vectorized Feb 1, 2022
2052461
Moved burn function into ERC721ABurnable
Vectorized Feb 1, 2022
33d4b41
Remove redundant burn check in ownershipOf
Vectorized Feb 1, 2022
5a6c851
Optimized ownershipOf
Vectorized Feb 1, 2022
a2ef813
Removed aux from AddressData for future PR
Vectorized Feb 1, 2022
07573e9
Packed currentIndex and totalBurned for gas savings
Vectorized Feb 1, 2022
bfd6d97
Added gas optimizations
Vectorized Feb 2, 2022
8fc600f
Added gas optimizations
Vectorized Feb 2, 2022
e23e803
Merge branch 'main' into feature/burnable
Vectorized Feb 2, 2022
8ff5722
Added requested changes
Vectorized Feb 3, 2022
a66b24b
Merge branch 'chiru-labs:main' into feature/burnable
Vectorized Feb 3, 2022
e1df945
Edited comments.
Vectorized Feb 3, 2022
9820bcb
Renamed totalBurned to burnedCounter
Vectorized Feb 4, 2022
7a02958
Renamed to burnCounter
Vectorized Feb 5, 2022
1cf3bc1
Updated comments.
Vectorized Feb 7, 2022
f25c639
Mark transferFrom/safeTransferFrom virtual
Vectorized Feb 7, 2022
92c82e1
Mark transferFrom/safeTransferFrom virtual
Vectorized Feb 7, 2022
66b488f
Updated comments.
Vectorized Feb 7, 2022
39ff829
Tidy up tests
Vectorized Feb 7, 2022
212912c
Inlined _exists for _burn and _transfer.
Vectorized Feb 7, 2022
bcbfb94
Merged custom errors
Vectorized Feb 8, 2022
1f3bc9e
Merged custom errors
Vectorized Feb 8, 2022
a06b5bc
Merge branch 'main' into feature/burnable
Vectorized Feb 8, 2022
64bc7e7
Fixed missing change from #59
Vectorized Feb 9, 2022
01e7ade
Gas optimization
Vectorized Feb 9, 2022
2e4857d
update specs for _beforeTokenTransfers and _afterTokenTransfers hooks
ahbanavi Feb 9, 2022
5ac0204
Added #84
Vectorized Feb 9, 2022
53ca717
Added #87
Vectorized Feb 9, 2022
d08c926
Merge branch 'main' into feature/burnable
Vectorized Feb 9, 2022
c493bf1
Added #85
Vectorized Feb 9, 2022
492ec57
Merge branch 'main' into feature/burnable
Vectorized Feb 10, 2022
7cf1fca
Merge branch 'feature/burnable' into feature/burnable
Vectorized Feb 10, 2022
1aeb1b8
Merge pull request #1 from ahbanavi/feature/burnable
Vectorized Feb 10, 2022
1bc34da
Added #89
Vectorized Feb 10, 2022
6e1ab7a
Added comments on packing _currentIndex and _burnCounter
Vectorized Feb 11, 2022
9ee026c
Removed overflow check for updatedIndex
Vectorized Feb 11, 2022
314faba
Added requested test changes
Vectorized Feb 12, 2022
c539195
Removed unused variable in burn test
Vectorized Feb 12, 2022
ca9bb69
Removed unused variable in burn test
Vectorized Feb 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ contract Azuki is ERC721A {

## Roadmap

- [] Add burn function
- [] Add flexibility for the first token id to not start at 0
- [] Support ERC721 Upgradeable
- [] Add more documentation on benefits of using ERC721A
Expand Down
193 changes: 159 additions & 34 deletions contracts/ERC721A.sol

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions contracts/extensions/ERC721ABurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../ERC721A.sol';
import '@openzeppelin/contracts/utils/Context.sol';

/**
* @title ERC721A Burnable Token
* @dev ERC721A Token that can be irreversibly burned (destroyed).
*/
abstract contract ERC721ABurnable is Context, ERC721A {

/**
* @dev Burns `tokenId`. See {ERC721A-_burn}.
*
* Requirements:
*
* - The caller must own `tokenId` or be an approved operator.
*/
function burn(uint256 tokenId) public virtual {
TokenOwnership memory prevOwnership = ownershipOf(tokenId);

bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr ||
isApprovedForAll(prevOwnership.addr, _msgSender()) ||
getApproved(tokenId) == _msgSender());

if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved();
Comment on lines +25 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason that you check for this here instead of internal _burn function?

Copy link
Collaborator Author

@Vectorized Vectorized Feb 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with OpenZeppelin’s implementation.


_burn(tokenId);
}
}
2 changes: 1 addition & 1 deletion contracts/extensions/ERC721AOwnersExplicit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
}

for (uint256 i = _nextOwnerToExplicitlySet; i <= endIndex; i++) {
if (_ownerships[i].addr == address(0)) {
if (_ownerships[i].addr == address(0) && !_ownerships[i].burned) {
TokenOwnership memory ownership = ownershipOf(i);
_ownerships[i].addr = ownership.addr;
_ownerships[i].startTimestamp = ownership.startTimestamp;
Expand Down
22 changes: 22 additions & 0 deletions contracts/mocks/ERC721ABurnableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/ERC721ABurnable.sol';

contract ERC721ABurnableMock is ERC721A, ERC721ABurnable {
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}

function exists(uint256 tokenId) public view returns (bool) {
return _exists(tokenId);
}

function safeMint(address to, uint256 quantity) public {
_safeMint(to, quantity);
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
}
}
27 changes: 27 additions & 0 deletions contracts/mocks/ERC721ABurnableOwnersExplicitMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/ERC721ABurnable.sol';
import '../extensions/ERC721AOwnersExplicit.sol';

contract ERC721ABurnableOwnersExplicitMock is ERC721A, ERC721ABurnable, ERC721AOwnersExplicit {
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}

function exists(uint256 tokenId) public view returns (bool) {
return _exists(tokenId);
}

function safeMint(address to, uint256 quantity) public {
_safeMint(to, quantity);
}

function setOwnersExplicit(uint256 quantity) public {
_setOwnersExplicit(quantity);
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 157 additions & 0 deletions test/extensions/ERC721ABurnable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
const { expect } = require('chai');

describe('ERC721ABurnable', function () {

beforeEach(async function () {
this.ERC721ABurnable = await ethers.getContractFactory('ERC721ABurnableMock');
this.token = await this.ERC721ABurnable.deploy('Azuki', 'AZUKI');
await this.token.deployed();
});

beforeEach(async function () {
const [owner, addr1, addr2] = await ethers.getSigners();
this.owner = owner;
this.addr1 = addr1;
this.addr2 = addr2;
this.numTestTokens = 10;
this.burnedTokenId = 5;
await this.token['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens);
await this.token.connect(this.addr1).burn(this.burnedTokenId);
});

it('changes exists', async function () {
expect(await this.token.exists(this.burnedTokenId)).to.be.false;
});

it('cannot burn a non-existing token', async function () {
const query = this.token.connect(this.addr1).burn(this.numTestTokens);
await expect(query).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
);
});

it('cannot burn a burned token', async function () {
const query = this.token.connect(this.addr1).burn(this.burnedTokenId);
await expect(query).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
);
})

it('cannot transfer a burned token', async function () {
const query = this.token.connect(this.addr1)
.transferFrom(this.addr1.address, this.addr2.address, this.burnedTokenId);
await expect(query).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
);
})

it('reduces totalSupply', async function () {
const supplyBefore = await this.token.totalSupply();
for (let i = 0; i < 2; ++i) {
await this.token.connect(this.addr1).burn(i);
expect(supplyBefore - (await this.token.totalSupply())).to.equal(i + 1);
}
})

it('adjusts owners tokens by index', async function () {
const n = await this.token.totalSupply();
for (let i = 0; i < this.burnedTokenId; ++i) {
expect(await this.token.tokenByIndex(i)).to.be.equal(i);
}
for (let i = this.burnedTokenId; i < n; ++i) {
expect(await this.token.tokenByIndex(i)).to.be.equal(i + 1);
}
// tokenIds of addr1: [0,1,2,3,4,6,7,8,9]
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
.to.be.equal(2);
await this.token.connect(this.addr1).burn(2);
// tokenIds of addr1: [0,1,3,4,6,7,8,9]
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
.to.be.equal(3);
await this.token.connect(this.addr1).burn(0);
// tokenIds of addr1: [1,3,4,6,7,8,9]
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
.to.be.equal(4);
await this.token.connect(this.addr1).burn(3);
// tokenIds of addr1: [1,4,6,7,8,9]
expect(await this.token.tokenOfOwnerByIndex(this.addr1.address, 2))
.to.be.equal(6);
})

it('adjusts owners balances', async function () {
expect(await this.token.balanceOf(this.addr1.address))
.to.be.equal(this.numTestTokens - 1);
});

it('adjusts token by index', async function () {
const n = await this.token.totalSupply();
for (let i = 0; i < this.burnedTokenId; ++i) {
expect(await this.token.tokenByIndex(i)).to.be.equal(i);
}
for (let i = this.burnedTokenId; i < n; ++i) {
expect(await this.token.tokenByIndex(i)).to.be.equal(i + 1);
}
await expect(this.token.tokenByIndex(n)).to.be.revertedWith(
'TokenIndexOutOfBounds'
);
});

describe('ownerships correctly set', async function () {
it('with token before previously burnt token transfered and burned', async function () {
const tokenIdToBurn = this.burnedTokenId - 1;
await this.token.connect(this.addr1)
.transferFrom(this.addr1.address, this.addr2.address, tokenIdToBurn);
expect(await this.token.ownerOf(tokenIdToBurn)).to.be.equal(this.addr2.address);
await this.token.connect(this.addr2).burn(tokenIdToBurn);
for (let i = 0; i < this.numTestTokens; ++i) {
if (i == tokenIdToBurn || i == this.burnedTokenId) {
await expect(this.token.ownerOf(i)).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
);
} else {
expect(await this.token.ownerOf(i)).to.be.equal(this.addr1.address);
}
}
});

it('with token after previously burnt token transfered and burned', async function () {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test to validate that transferring a burnt token gets reverted?

const tokenIdToBurn = this.burnedTokenId + 1;
await this.token.connect(this.addr1)
.transferFrom(this.addr1.address, this.addr2.address, tokenIdToBurn);
expect(await this.token.ownerOf(tokenIdToBurn)).to.be.equal(this.addr2.address);
await this.token.connect(this.addr2).burn(tokenIdToBurn);
for (let i = 0; i < this.numTestTokens; ++i) {
if (i == tokenIdToBurn || i == this.burnedTokenId) {
await expect(this.token.ownerOf(i)).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
)
} else {
expect(await this.token.ownerOf(i)).to.be.equal(this.addr1.address);
}
}
});

it('with first token burned', async function () {
await this.token.connect(this.addr1).burn(0);
for (let i = 0; i < this.numTestTokens; ++i) {
if (i == 0 || i == this.burnedTokenId) {
await expect(this.token.ownerOf(i)).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
)
} else {
expect(await this.token.ownerOf(i)).to.be.equal(this.addr1.address);
}
}
});

it('with last token burned', async function () {
await expect(this.token.ownerOf(this.numTestTokens)).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
)
await this.token.connect(this.addr1).burn(this.numTestTokens - 1);
await expect(this.token.ownerOf(this.numTestTokens - 1)).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
)
});
});
});
45 changes: 45 additions & 0 deletions test/extensions/ERC721ABurnableOwnersExplicit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { expect } = require('chai');
const { constants } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;

describe('ERC721ABurnableOwnersExplicit', function () {
beforeEach(async function () {
this.ERC721ABurnableOwnersExplicit = await ethers.getContractFactory('ERC721ABurnableOwnersExplicitMock');
this.token = await this.ERC721ABurnableOwnersExplicit.deploy('Azuki', 'AZUKI');
await this.token.deployed();
});

beforeEach(async function () {
const [owner, addr1, addr2, addr3] = await ethers.getSigners();
this.owner = owner;
this.addr1 = addr1;
this.addr2 = addr2;
this.addr3 = addr3;
await this.token['safeMint(address,uint256)'](addr1.address, 1);
await this.token['safeMint(address,uint256)'](addr2.address, 2);
await this.token['safeMint(address,uint256)'](addr3.address, 3);
await this.token.connect(this.addr1).burn(0);
await this.token.connect(this.addr3).burn(4);
await this.token.setOwnersExplicit(6);
});

it('ownerships correctly set', async function () {
for (let tokenId = 0; tokenId < 6; tokenId++) {
let owner = await this.token.getOwnershipAt(tokenId);
expect(owner[0]).to.not.equal(ZERO_ADDRESS);
if (tokenId == 0 || tokenId == 4) {
expect(owner[2]).to.equal(true);
await expect(this.token.ownerOf(tokenId)).to.be.revertedWith(
'OwnerQueryForNonexistentToken'
)
} else {
expect(owner[2]).to.equal(false);
if (tokenId < 1+2) {
expect(await this.token.ownerOf(tokenId)).to.be.equal(this.addr2.address);
} else {
expect(await this.token.ownerOf(tokenId)).to.be.equal(this.addr3.address);
}
}
}
});
});