Skip to content

Commit

Permalink
Use uint packing instead of structs (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized authored May 17, 2022
1 parent e7c1e8c commit 3783cc4
Show file tree
Hide file tree
Showing 15 changed files with 341 additions and 140 deletions.
333 changes: 236 additions & 97 deletions contracts/ERC721A.sol

Large diffs are not rendered by default.

17 changes: 1 addition & 16 deletions contracts/IERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ interface IERC721A {
*/
error URIQueryForNonexistentToken();

// Compiler will pack this into a single 256bit word.
struct TokenOwnership {
// The address of the owner.
address addr;
Expand All @@ -83,23 +82,9 @@ interface IERC721A {
bool burned;
}

// Compiler will pack this into a single 256bit word.
struct AddressData {
// Realistically, 2**64-1 is more than enough.
uint64 balance;
// Keeps track of mint count with minimal overhead for tokenomics.
uint64 numberMinted;
// Keeps track of burn count with minimal overhead for tokenomics.
uint64 numberBurned;
// For miscellaneous variable(s) pertaining to the address
// (e.g. number of whitelist mint slots used).
// If there are multiple variables, please pack them into a uint64.
uint64 aux;
}

/**
* @dev Returns the total amount of tokens stored by the contract.
*
*
* Burned tokens are calculated here, use `_totalMinted()` if you want to count just minted tokens.
*/
function totalSupply() external view returns (uint256);
Expand Down
10 changes: 3 additions & 7 deletions contracts/extensions/ERC721AOwnersExplicit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
* No more ownership slots to explicity initialize.
*/
error AllOwnershipsHaveBeenSet();

/**
* The `quantity` must be more than zero.
*/
error QuantityMustBeNonZero();

/**
* At least one token needs to be minted.
*/
Expand Down Expand Up @@ -47,11 +47,7 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
}

for (uint256 i = _nextOwnerToExplicitlySet; i <= endIndex; i++) {
if (_ownerships[i].addr == address(0) && !_ownerships[i].burned) {
TokenOwnership memory ownership = _ownershipOf(i);
_ownerships[i].addr = ownership.addr;
_ownerships[i].startTimestamp = ownership.startTimestamp;
}
_initializeOwnershipAt(i);
}

nextOwnerToExplicitlySet = endIndex + 1;
Expand Down
6 changes: 3 additions & 3 deletions contracts/extensions/ERC721AQueryable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable {
if (tokenId < _startTokenId() || tokenId >= _currentIndex) {
return ownership;
}
ownership = _ownerships[tokenId];
ownership = _ownershipAt(tokenId);
if (ownership.burned) {
return ownership;
}
Expand Down Expand Up @@ -111,7 +111,7 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable {
currOwnershipAddr = ownership.addr;
}
for (uint256 i = start; i != stop && tokenIdsIdx != tokenIdsMaxLength; ++i) {
ownership = _ownerships[i];
ownership = _ownershipAt(i);
if (ownership.burned) {
continue;
}
Expand Down Expand Up @@ -148,7 +148,7 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable {
uint256[] memory tokenIds = new uint256[](tokenIdsLength);
TokenOwnership memory ownership;
for (uint256 i = _startTokenId(); tokenIdsIdx != tokenIdsLength; ++i) {
ownership = _ownerships[i];
ownership = _ownershipAt(i);
if (ownership.burned) {
continue;
}
Expand Down
6 changes: 5 additions & 1 deletion contracts/mocks/ERC721ABurnableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ contract ERC721ABurnableMock is ERC721A, ERC721ABurnable {
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
return _ownershipAt(index);
}

function totalMinted() public view returns (uint256) {
return _totalMinted();
}

function numberBurned(address owner) public view returns (uint256) {
return _numberBurned(owner);
}
}
2 changes: 1 addition & 1 deletion contracts/mocks/ERC721ABurnableOwnersExplicitMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ contract ERC721ABurnableOwnersExplicitMock is ERC721A, ERC721ABurnable, ERC721AO
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
return _ownershipAt(index);
}
}
4 changes: 4 additions & 0 deletions contracts/mocks/ERC721AMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,8 @@ contract ERC721AMock is ERC721A {
function toString(uint256 x) public pure returns (string memory) {
return _toString(x);
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownershipAt(index);
}
}
4 changes: 2 additions & 2 deletions contracts/mocks/ERC721AOwnersExplicitMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract ERC721AOwnersExplicitMock is ERC721AOwnersExplicit {
_setOwnersExplicit(quantity);
}

function getOwnershipAt(uint256 tokenId) public view returns (TokenOwnership memory) {
return _ownerships[tokenId];
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownershipAt(index);
}
}
2 changes: 1 addition & 1 deletion contracts/mocks/ERC721AQueryableOwnersExplicitMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ contract ERC721AQueryableOwnersExplicitMock is ERC721AQueryableMock, ERC721AOwne
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
return _ownershipAt(index);
}
}
18 changes: 17 additions & 1 deletion test/ERC721A.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { deployContract } = require('./helpers.js');
const { deployContract, getBlockTimestamp, mineBlockTimestamp } = require('./helpers.js');
const { expect } = require('chai');
const { BigNumber } = require('ethers');
const { constants } = require('@openzeppelin/test-helpers');
Expand Down Expand Up @@ -205,7 +205,17 @@ const createTestSuite = ({ contract, constructorArgs }) =>
this.from = sender.address;
this.to = this.receiver.address;
await this.erc721a.connect(sender).setApprovalForAll(this.to, true);

const ownershipBefore = await this.erc721a.getOwnershipAt(this.tokenId);
this.timestampBefore = parseInt(ownershipBefore.startTimestamp);
this.timestampToMine = (await getBlockTimestamp()) + 100;
await mineBlockTimestamp(this.timestampToMine);
this.timestampMined = await getBlockTimestamp();

this.transferTx = await this.erc721a.connect(sender)[transferFn](this.from, this.to, this.tokenId);

const ownershipAfter = await this.erc721a.getOwnershipAt(this.tokenId);
this.timestampAfter = parseInt(ownershipAfter.startTimestamp);
});

it('transfers the ownership of the given token ID to the given address', async function () {
Expand All @@ -225,6 +235,12 @@ const createTestSuite = ({ contract, constructorArgs }) =>
it('adjusts owners balances', async function () {
expect(await this.erc721a.balanceOf(this.from)).to.be.equal(1);
});

it('startTimestamp updated correctly', async function () {
expect(this.timestampBefore).to.be.lt(this.timestampToMine);
expect(this.timestampAfter).to.be.gte(this.timestampToMine);
expect(this.timestampToMine).to.be.eq(this.timestampMined);
});
};

const testUnsuccessfulTransfer = function (transferFn) {
Expand Down
11 changes: 11 additions & 0 deletions test/GasUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,15 @@ describe('ERC721A Gas Usage', function () {
}
});
});

context('transferFrom', function () {
it('transfer to and from two addresses', async function () {
await this.erc721a.mintTen(this.addr1.address);
await this.erc721a.mintTen(this.owner.address);
for (let i = 0; i < 10; ++i) {
await this.erc721a.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 1);
await this.erc721a.connect(this.owner).transferFrom(this.owner.address, this.addr1.address, 1);
}
});
});
});
23 changes: 22 additions & 1 deletion test/extensions/ERC721ABurnable.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { deployContract } = require('../helpers.js');
const { deployContract, getBlockTimestamp, mineBlockTimestamp } = require('../helpers.js');
const { expect } = require('chai');
const { constants } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
Expand Down Expand Up @@ -44,6 +44,12 @@ const createTestSuite = ({ contract, constructorArgs }) =>
});
});

it('changes numberBurned', async function () {
expect(await this.erc721aBurnable.numberBurned(this.addr1.address)).to.equal(1);
await this.erc721aBurnable.connect(this.addr1).burn(this.startTokenId);
expect(await this.erc721aBurnable.numberBurned(this.addr1.address)).to.equal(2);
});

it('changes exists', async function () {
expect(await this.erc721aBurnable.exists(this.burnedTokenId)).to.be.false;
expect(await this.erc721aBurnable.exists(this.notBurnedTokenId)).to.be.true;
Expand Down Expand Up @@ -106,6 +112,21 @@ const createTestSuite = ({ contract, constructorArgs }) =>
expect(await this.erc721aBurnable.balanceOf(this.addr1.address)).to.be.equal(this.numTestTokens - 1);
});

it('startTimestamp updated correctly', async function () {
const tokenIdToBurn = this.burnedTokenId + 1;
const ownershipBefore = await this.erc721aBurnable.getOwnershipAt(tokenIdToBurn);
const timestampBefore = parseInt(ownershipBefore.startTimestamp);
const timestampToMine = (await getBlockTimestamp()) + 100;
await mineBlockTimestamp(timestampToMine);
const timestampMined = await getBlockTimestamp();
await this.erc721aBurnable.connect(this.addr1).burn(tokenIdToBurn);
const ownershipAfter = await this.erc721aBurnable.getOwnershipAt(tokenIdToBurn);
const timestampAfter = parseInt(ownershipAfter.startTimestamp);
expect(timestampBefore).to.be.lt(timestampToMine);
expect(timestampAfter).to.be.gte(timestampToMine);
expect(timestampToMine).to.be.eq(timestampMined);
});

describe('ownerships correctly set', async function () {
it('with token before previously burnt token transferred and burned', async function () {
const tokenIdToBurn = this.burnedTokenId - 1;
Expand Down
18 changes: 17 additions & 1 deletion test/extensions/ERC721AOwnersExplicit.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { deployContract } = require('../helpers.js');
const { deployContract, getBlockTimestamp, mineBlockTimestamp } = require('../helpers.js');
const { expect } = require('chai');
const { constants } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
Expand Down Expand Up @@ -27,6 +27,12 @@ const createTestSuite = ({ contract, constructorArgs }) =>
this.addr1 = addr1;
this.addr2 = addr2;
this.addr3 = addr3;

this.timestampBefore = await getBlockTimestamp();
this.timestampToMine = (this.timestampBefore) + 100;
await mineBlockTimestamp(this.timestampToMine);
this.timestampMined = await getBlockTimestamp();

// After the following mints, our ownership array will look like this:
// | 1 | 2 | Empty | 3 | Empty | Empty |
await this.erc721aOwnersExplicit['safeMint(address,uint256)'](addr1.address, 1);
Expand Down Expand Up @@ -95,6 +101,16 @@ const createTestSuite = ({ contract, constructorArgs }) =>
'AllOwnershipsHaveBeenSet'
);
});

it('sets startTimestamps correctly', async function () {
await this.erc721aOwnersExplicit.setOwnersExplicit(6);
expect(this.timestampToMine).to.be.eq(this.timestampMined);
for (let tokenId = this.startTokenId; tokenId < 6 + this.startTokenId; tokenId++) {
let owner = await this.erc721aOwnersExplicit.getOwnershipAt(tokenId);
expect(owner.startTimestamp).to.be.gt(this.timestampBefore);
expect(owner.startTimestamp).to.be.gte(this.timestampToMine);
}
});
});
});
});
Expand Down
16 changes: 8 additions & 8 deletions test/extensions/ERC721AQueryable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@ const createTestSuite = ({ contract, constructorArgs, setOwnersExplicit = false
const tokenIds = [].concat(this.owner.expected.tokens, this.addr3.expected.tokens);
const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds);
for (let i = 0; i < tokenIds.length; ++i) {
const tokenId = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], tokenId);
const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], owner);
}
});

Expand All @@ -263,8 +263,8 @@ const createTestSuite = ({ contract, constructorArgs, setOwnersExplicit = false
const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds);
expectExplicitOwnershipBurned(explicitOwnerships[0], this.owner.address);
for (let i = 1; i < tokenIds.length; ++i) {
const tokenId = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], tokenId);
const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], owner);
}
});

Expand All @@ -274,8 +274,8 @@ const createTestSuite = ({ contract, constructorArgs, setOwnersExplicit = false
const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds);
expectExplicitOwnershipExists(explicitOwnerships[0], this.addr4.address);
for (let i = 1; i < tokenIds.length; ++i) {
const tokenId = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], tokenId);
const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], owner);
}
});

Expand All @@ -284,8 +284,8 @@ const createTestSuite = ({ contract, constructorArgs, setOwnersExplicit = false
const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds);
expectExplicitOwnershipNotExists(explicitOwnerships[0]);
for (let i = 1; i < tokenIds.length; ++i) {
const tokenId = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], tokenId);
const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]);
expectExplicitOwnershipExists(explicitOwnerships[i], owner);
}
});
});
Expand Down
11 changes: 10 additions & 1 deletion test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ const deployContract = async function (contractName, constructorArgs) {
return contract;
};

module.exports = { deployContract };
const getBlockTimestamp = async function () {
return parseInt((await ethers.provider.getBlock('latest'))['timestamp']);
};

const mineBlockTimestamp = async function (timestamp) {
await ethers.provider.send('evm_setNextBlockTimestamp', [timestamp]);
await ethers.provider.send('evm_mine');
};

module.exports = { deployContract, getBlockTimestamp, mineBlockTimestamp };

0 comments on commit 3783cc4

Please sign in to comment.