Skip to content

Commit

Permalink
Merge branch 'main' into rewrite_delete
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized authored Jun 14, 2022
2 parents 79e91e2 + f6cc94d commit 4d33d6c
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 0 deletions.
17 changes: 17 additions & 0 deletions contracts/IERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,20 @@ interface IERC721A {
*/
error URIQueryForNonexistentToken();

/**
* The `quantity` minted with ERC2309 exceeds the safety limit.
*/
error MintERC2309QuantityExceedsLimit();

struct TokenOwnership {
// The address of the owner.
address addr;
// Keeps track of the start time of ownership with minimal overhead for tokenomics.
uint64 startTimestamp;
// Whether the token has been burned.
bool burned;
// Arbitrary data similar to `startTimestamp` that can be set through `_extraData`.
uint24 extraData;
}

/**
Expand Down Expand Up @@ -252,4 +259,14 @@ interface IERC721A {
* @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
*/
function tokenURI(uint256 tokenId) external view returns (string memory);

// ==============================
// IERC2309
// ==============================

/**
* @dev Emitted when tokens in `fromTokenId` to `toTokenId` (inclusive) is transferred from `from` to `to`,
* as defined in the ERC2309 standard. See `_mintERC2309` for more details.
*/
event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);
}
3 changes: 3 additions & 0 deletions contracts/extensions/ERC721AQueryable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable {
* - `addr` = `address(0)`
* - `startTimestamp` = `0`
* - `burned` = `false`
* - `extraData` = `0`
*
* If the `tokenId` is burned:
* - `addr` = `<Address of owner before token was burned>`
* - `startTimestamp` = `<Timestamp when token was burned>`
* - `burned = `true`
* - `extraData` = `<Extra data when token was burned>`
*
* Otherwise:
* - `addr` = `<Address of owner>`
* - `startTimestamp` = `<Timestamp of start of ownership>`
* - `burned = `false`
* - `extraData` = `<Extra data at start of ownership>`
*/
function explicitOwnershipOf(uint256 tokenId) public view override returns (TokenOwnership memory) {
TokenOwnership memory ownership;
Expand Down
25 changes: 25 additions & 0 deletions contracts/mocks/ERC721ATransferCounterMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.0.0
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import './ERC721AMock.sol';

contract ERC721ATransferCounterMock is ERC721AMock {
constructor(string memory name_, string memory symbol_) ERC721AMock(name_, symbol_) {}

function _extraData(
address from,
address to,
uint24 previousExtraData
) internal view virtual override returns (uint24) {
if (from == address(0)) {
return 42;
}
if (to == address(0)) {
return 1337;
}
return previousExtraData + 1;
}
}
39 changes: 39 additions & 0 deletions contracts/mocks/ERC721AWithERC2309Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.0.0
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../ERC721A.sol';

contract ERC721AWithERC2309Mock is ERC721A {
constructor(
string memory name_,
string memory symbol_,
address to,
uint256 quantity,
bool mintInConstructor
) ERC721A(name_, symbol_) {
if (mintInConstructor) {
_mintERC2309(to, quantity);
}
}

/**
* @dev This function is only for gas comparison purposes.
* Calling `_mintERC3201` outside of contract creation is non-compliant
* with the ERC721 standard.
*/
function mintOneERC2309(address to) public {
_mintERC2309(to, 1);
}

/**
* @dev This function is only for gas comparison purposes.
* Calling `_mintERC3201` outside of contract creation is non-compliant
* with the ERC721 standard.
*/
function mintTenERC2309(address to) public {
_mintERC2309(to, 10);
}
}
50 changes: 50 additions & 0 deletions test/ERC721A.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,53 @@ describe(
'ERC721A override _startTokenId()',
createTestSuite({ contract: 'ERC721AStartTokenIdMock', constructorArgs: ['Azuki', 'AZUKI', 1] })
);

describe('ERC721A with ERC2309', async function () {
beforeEach(async function () {
const [owner, addr1] = await ethers.getSigners();
this.owner = owner;
this.addr1 = addr1;

let args;
args = ['Azuki', 'AZUKI', this.owner.address, 1, true];
this.erc721aMint1 = await deployContract('ERC721AWithERC2309Mock', args);
args = ['Azuki', 'AZUKI', this.owner.address, 10, true];
this.erc721aMint10 = await deployContract('ERC721AWithERC2309Mock', args);
});

it('emits a ConsecutiveTransfer event for single mint', async function () {
expect(this.erc721aMint1.deployTransaction)
.to.emit(this.erc721aMint1, 'ConsecutiveTransfer')
.withArgs(0, 0, ZERO_ADDRESS, this.owner.address);
});

it('emits a ConsecutiveTransfer event for a batch mint', async function () {
expect(this.erc721aMint10.deployTransaction)
.to.emit(this.erc721aMint10, 'ConsecutiveTransfer')
.withArgs(0, 9, ZERO_ADDRESS, this.owner.address);
});

it('requires quantity to be below mint limit', async function () {
let args;
const mintLimit = 5000;
args = ['Azuki', 'AZUKI', this.owner.address, mintLimit, true];
await deployContract('ERC721AWithERC2309Mock', args);
args = ['Azuki', 'AZUKI', this.owner.address, mintLimit + 1, true];
await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintERC2309QuantityExceedsLimit');
})

it('rejects mints to the zero address', async function () {
let args = ['Azuki', 'AZUKI', ZERO_ADDRESS, 1, true];
await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintToZeroAddress');
});

it('requires quantity to be greater than 0', async function () {
let args = ['Azuki', 'AZUKI', this.owner.address, 0, true];
await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintZeroQuantity');
});
});

describe(
'ERC721A override _extraData()',
createTestSuite({ contract: 'ERC721ATransferCounterMock', constructorArgs: ['Azuki', 'AZUKI'] })
);
22 changes: 22 additions & 0 deletions test/GasUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,26 @@ describe('ERC721A Gas Usage', function () {
await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address);
});
});

it('mintOneERC2309', async function () {
// The following call `_mintERC3201` outside of contract creation.
// This is non-compliant with the ERC721 standard,
// and is only meant for gas comparisons.
let args = ['Azuki', 'AZUKI', this.owner.address, 0, false];
let contract = await deployContract('ERC721AWithERC2309Mock', args);
await contract.mintOneERC2309(this.owner.address);
await contract.mintOneERC2309(this.owner.address);
await contract.mintOneERC2309(this.addr1.address);
});

it('mintTenERC2309', async function () {
// The following call `_mintERC3201` outside of contract creation.
// This is non-compliant with the ERC721 standard,
// and is only meant for gas comparisons.
let args = ['Azuki', 'AZUKI', this.owner.address, 0, false];
let contract = await deployContract('ERC721AWithERC2309Mock', args);
await contract.mintTenERC2309(this.owner.address);
await contract.mintTenERC2309(this.owner.address);
await contract.mintTenERC2309(this.addr1.address);
});
});
3 changes: 3 additions & 0 deletions test/extensions/ERC721AQueryable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@ const createTestSuite = ({ contract, constructorArgs }) =>
expect(explicitOwnership.burned).to.eql(true);
expect(explicitOwnership.addr).to.eql(address);
expect(explicitOwnership.startTimestamp).to.not.eql(BigNumber.from(0));
expect(explicitOwnership.extraData).to.equal(BigNumber.from(0));
};

const expectExplicitOwnershipNotExists = function (explicitOwnership) {
expect(explicitOwnership.burned).to.eql(false);
expect(explicitOwnership.addr).to.eql(ZERO_ADDRESS);
expect(explicitOwnership.startTimestamp).to.eql(BigNumber.from(0));
expect(explicitOwnership.extraData).to.equal(BigNumber.from(0));
};

const expectExplicitOwnershipExists = function (explicitOwnership, address) {
expect(explicitOwnership.burned).to.eql(false);
expect(explicitOwnership.addr).to.eql(address);
expect(explicitOwnership.startTimestamp).to.not.eql(BigNumber.from(0));
expect(explicitOwnership.extraData).to.equal(BigNumber.from(0));
};

context('with no minted tokens', async function () {
Expand Down
97 changes: 97 additions & 0 deletions test/extensions/ERC721ATransferCounter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const { deployContract, offsettedIndex } = require('../helpers.js');
const { expect } = require('chai');

const createTestSuite = ({ contract, constructorArgs }) =>
function () {
let offsetted;

context(`${contract}`, function () {
beforeEach(async function () {
this.erc721aCounter = await deployContract(contract, constructorArgs);

this.startTokenId = this.erc721aCounter.startTokenId
? (await this.erc721aCounter.startTokenId()).toNumber()
: 0;

offsetted = (...arr) => offsettedIndex(this.startTokenId, arr);
});

context('with minted tokens', async function () {
beforeEach(async function () {
const [owner, addr1] = await ethers.getSigners();
this.owner = owner;
this.addr1 = addr1;

this.addr1.expected = {
balance: 1,
tokens: [offsetted(0)],
};

this.owner.expected = {
balance: 2,
tokens: offsetted(1, 2),
};

this.mintOrder = [this.addr1, this.owner];

for (const minter of this.mintOrder) {
const balance = minter.expected.balance;
if (balance > 0) {
await this.erc721aCounter['safeMint(address,uint256)'](minter.address, balance);
}
// sanity check
expect(await this.erc721aCounter.balanceOf(minter.address)).to.equal(minter.expected.balance);
}
});

describe('_ownershipOf', function () {
it('initial', async function () {
for (const minter of this.mintOrder) {
for (const tokenId in minter.expected.tokens) {
const ownership = await this.erc721aCounter.getOwnershipOf(tokenId);
expect(ownership.extraData).to.equal(42);
}
}
});

it('after a transfer', async function () {
await this.erc721aCounter.transferFrom(this.owner.address, this.addr1.address, 1);

const tests = [
{ tokenId: 0, expectedData: 42 },
{ tokenId: 1, expectedData: 43 },
{ tokenId: 2, expectedData: 42 },
];

for (const test of tests) {
const ownership = await this.erc721aCounter.getOwnershipOf(test.tokenId);
expect(ownership.extraData).to.equal(test.expectedData);
}
});

it('after a burn', async function () {
await this.erc721aCounter['burn(uint256)'](2);

const tests = [
{ tokenId: 0, expectedData: 42 },
{ tokenId: 1, expectedData: 42 },
{ tokenId: 2, expectedData: 1337 },
];

for (const test of tests) {
const ownership = await this.erc721aCounter.getOwnershipAt(test.tokenId);
expect(ownership.extraData).to.equal(test.expectedData);
}
});
});
});
});
};

describe(
'ERC721A override _extraData()',
createTestSuite({
contract: 'ERC721ATransferCounterMock',
constructorArgs: ['Azuki', 'AZUKI'],
})
);

0 comments on commit 4d33d6c

Please sign in to comment.