Skip to content

Commit

Permalink
Merge pull request #1 from Vectorized/feature/startIndex
Browse files Browse the repository at this point in the history
Feature/start index + Burnable
  • Loading branch information
Pczek authored Feb 14, 2022
2 parents 42e2ad3 + ca96c35 commit f51b943
Show file tree
Hide file tree
Showing 12 changed files with 462 additions and 53 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +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
- [] Increase test coverage
Expand Down
202 changes: 164 additions & 38 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();

_burn(tokenId);
}
}
10 changes: 5 additions & 5 deletions contracts/extensions/ERC721AOwnersExplicit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,25 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
*/
function _setOwnersExplicit(uint256 quantity) internal {
if (quantity == 0) revert QuantityMustBeNonZero();
if (_nextTokenId == _startTokenId()) revert NoTokensMintedYet();
if (_currentIndex == _startTokenId()) revert NoTokensMintedYet();
uint256 _nextOwnerToExplicitlySet = nextOwnerToExplicitlySet;
if (_nextOwnerToExplicitlySet == 0) {
_nextOwnerToExplicitlySet = _startTokenId();
}
if (_nextOwnerToExplicitlySet >= _nextTokenId) revert AllOwnershipsHaveBeenSet();
if (_nextOwnerToExplicitlySet >= _currentIndex) revert AllOwnershipsHaveBeenSet();

// Index underflow is impossible.
// Counter or index overflow is incredibly unrealistic.
unchecked {
uint256 endIndex = _nextOwnerToExplicitlySet + quantity - 1;

// Set the end index to be the last token index
if (endIndex + 1 > _nextTokenId) {
endIndex = _nextTokenId - 1;
if (endIndex + 1 > _currentIndex) {
endIndex = _currentIndex - 1;
}

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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "erc721a",
"version": "2.1.0",
"version": "2.2.0",
"description": "ERC721A contract for Solidity",
"files": [
"/contracts/**/*.sol",
Expand Down
1 change: 1 addition & 0 deletions projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ Here are a list of projects that have or will be implementing ERC721A as part of
- [Turf](https://turf.dev/) | [Etherscan](https://etherscan.io/address/0x55d89273143de3de00822c9271dbcbd9b44b44c6) | [Twitter](https://twitter.com/turfnft)
- [Knit Kins](https://knitkins.com) | Etherscan | [Twitter](https://twitter.com/KnitKinsNFT)
- [Meta Angels NFT](https://www.metaangelsnft.com) | [Etherscan](https://etherscan.io/address/0xaD265Ab9B99296364F13Ce5b8B3e8d0998778bfb) | [Twitter](https://twitter.com/meta_angels)
- [Probably Something](https://probablysomething.io/) | [Etherscan](https://etherscan.io/address/0x0e6c54bdf6bfc75777c23dd2b7504d82b484582a) | [Twitter](https://twitter.com/ProblySomething)
10 changes: 5 additions & 5 deletions test/GasUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,31 @@ describe('ERC721A Gas Usage', function () {
context('mintOne', function () {
it('runs mintOne 50 times', async function () {
for (let i = 0; i < 50; i++) {
await this.erc721a.safeMintOne(this.addr1.address);
await this.erc721a.mintOne(this.addr1.address);
}
});
});

context('safeMintOne', function () {
it('runs safeMintOne 50 times', async function () {
for (let i = 0; i < 50; i++) {
await this.erc721a.mintOne(this.addr1.address);
await this.erc721a.safeMintOne(this.addr1.address);
}
});
});

context('mintTen', function () {
it('runs mintTen 50 times', async function () {
for (let i = 0; i < 50; i++) {
await this.erc721a.safeMintTen(this.addr1.address);
await this.erc721a.mintTen(this.addr1.address);
}
});
});

context('safeMintTen', function () {
it('runs mintTen 50 times', async function () {
it('runs safeMintTen 50 times', async function () {
for (let i = 0; i < 50; i++) {
await this.erc721a.mintTen(this.addr1.address);
await this.erc721a.safeMintTen(this.addr1.address);
}
});
});
Expand Down
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 () {
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);
}
}
}
});
});

0 comments on commit f51b943

Please sign in to comment.