-
Notifications
You must be signed in to change notification settings - Fork 842
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add tokensOfOwner function + tests + Append Divine Apples to projects.md * fix indentation to spaces * Add ERC721ALowCap extension/remove existing tokensOfOwner created previously * tabs -> spaces * remove divine apples from project.md * lint fixs + rename file for mock naming * fix indent in tests * use createTestSuite style * use _startTOkenId in erc721aLowCap * fix style * add offseted function * update offseted * WIP: add tests for low cap owners explicit * add tests for setOwnersExplicit * add tests for burnable * add minor check while minting * fix style * Fix lint error Co-authored-by: Austinhs <austin@webglitch.com> Co-authored-by: Amirhossein Banavi <ahbanavi@gmail.com>
- Loading branch information
1 parent
5e05378
commit 76076c5
Showing
5 changed files
with
229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Creator: Chiru Labs | ||
|
||
pragma solidity ^0.8.4; | ||
|
||
import '../ERC721A.sol'; | ||
|
||
/** | ||
* @title ERC721A Low Cap | ||
* @dev ERC721A Helper functions for Low Cap (<= 10,000) totalSupply. | ||
*/ | ||
abstract contract ERC721ALowCap is ERC721A { | ||
/** | ||
* @dev Returns the tokenIds of the address. O(totalSupply) in complexity. | ||
*/ | ||
function tokensOfOwner(address owner) public view returns (uint256[] memory) { | ||
uint256 holdingAmount = balanceOf(owner); | ||
uint256 currSupply = _currentIndex; | ||
uint256 tokenIdsIdx; | ||
address currOwnershipAddr; | ||
|
||
uint256[] memory list = new uint256[](holdingAmount); | ||
|
||
unchecked { | ||
for (uint256 i = _startTokenId(); i < currSupply; ++i) { | ||
TokenOwnership memory ownership = _ownerships[i]; | ||
|
||
if (ownership.burned) { | ||
continue; | ||
} | ||
|
||
// Find out who owns this sequence | ||
if (ownership.addr != address(0)) { | ||
currOwnershipAddr = ownership.addr; | ||
} | ||
|
||
// Append tokens the last found owner owns in the sequence | ||
if (currOwnershipAddr == owner) { | ||
list[tokenIdsIdx++] = i; | ||
} | ||
|
||
// All tokens have been found, we don't need to keep searching | ||
if (tokenIdsIdx == holdingAmount) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return list; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Creators: Chiru Labs | ||
|
||
pragma solidity ^0.8.4; | ||
|
||
import '../extensions/ERC721ALowCap.sol'; | ||
import '../extensions/ERC721ABurnable.sol'; | ||
|
||
contract ERC721ALowCapMock is ERC721ALowCap, ERC721ABurnable { | ||
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} | ||
|
||
function safeMint(address to, uint256 quantity) public { | ||
_safeMint(to, quantity); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Creators: Chiru Labs | ||
|
||
pragma solidity ^0.8.4; | ||
|
||
import './ERC721ALowCapMock.sol'; | ||
import '../extensions/ERC721AOwnersExplicit.sol'; | ||
|
||
contract ERC721ALowCapOwnersExplicitMock is ERC721ALowCapMock, ERC721AOwnersExplicit { | ||
constructor(string memory name_, string memory symbol_) ERC721ALowCapMock(name_, symbol_) {} | ||
|
||
function setOwnersExplicit(uint256 quantity) public { | ||
_setOwnersExplicit(quantity); | ||
} | ||
|
||
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { | ||
return _ownerships[index]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Creators: Chiru Labs | ||
|
||
pragma solidity ^0.8.4; | ||
|
||
import './ERC721ALowCapMock.sol'; | ||
import './StartTokenIdHelper.sol'; | ||
|
||
contract ERC721ALowCapStartTokenIdMock is StartTokenIdHelper, ERC721ALowCapMock { | ||
constructor( | ||
string memory name_, | ||
string memory symbol_, | ||
uint256 startTokenId_ | ||
) StartTokenIdHelper(startTokenId_) ERC721ALowCapMock(name_, symbol_) {} | ||
|
||
function _startTokenId() internal view override returns (uint256) { | ||
return startTokenId; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
const { deployContract } = require('../helpers.js'); | ||
const { expect } = require('chai'); | ||
const { BigNumber } = require('ethers'); | ||
const { constants } = require('@openzeppelin/test-helpers'); | ||
const { ZERO_ADDRESS } = constants; | ||
|
||
const createTestSuite = ({ contract, constructorArgs, setOwnersExplicit = false }) => | ||
function () { | ||
let offseted; | ||
|
||
context(`${contract}`, function () { | ||
beforeEach(async function () { | ||
this.erc721aLowCap = await deployContract(contract, constructorArgs); | ||
|
||
this.startTokenId = this.erc721aLowCap.startTokenId ? (await this.erc721aLowCap.startTokenId()).toNumber() : 0; | ||
offseted = (...arr) => arr.map((num) => BigNumber.from(this.startTokenId + num)); | ||
}); | ||
|
||
context('with minted tokens', async function () { | ||
beforeEach(async function () { | ||
const [owner, addr1, addr2, addr3, addr4] = await ethers.getSigners(); | ||
this.owner = owner; | ||
this.addr1 = addr1; | ||
this.addr2 = addr2; | ||
this.addr3 = addr3; | ||
this.addr4 = addr4; | ||
|
||
this.addr1.expected = { | ||
balance: 1, | ||
tokens: offseted(0), | ||
}; | ||
|
||
this.addr2.expected = { | ||
balance: 2, | ||
tokens: offseted(1, 2), | ||
}; | ||
|
||
this.addr3.expected = { | ||
balance: 3, | ||
tokens: offseted(3, 4, 5), | ||
}; | ||
|
||
this.addr4.expected = { | ||
balance: 0, | ||
tokens: [], | ||
}; | ||
|
||
this.owner.expected = { | ||
balance: 3, | ||
tokens: offseted(6, 7, 8), | ||
}; | ||
|
||
this.mintOrder = [this.addr1, this.addr2, this.addr3, this.addr4, owner]; | ||
|
||
for (const minter of this.mintOrder) { | ||
const balance = minter.expected.balance; | ||
if (balance > 0) { | ||
await this.erc721aLowCap['safeMint(address,uint256)'](minter.address, balance); | ||
} | ||
// sanity check | ||
expect(await this.erc721aLowCap.balanceOf(minter.address)).to.equal(minter.expected.balance); | ||
} | ||
|
||
if (setOwnersExplicit) { | ||
// sanity check | ||
expect((await this.erc721aLowCap.getOwnershipAt(offseted(4)[0]))[0]).to.equal(ZERO_ADDRESS); | ||
await this.erc721aLowCap.setOwnersExplicit(10); | ||
// again, sanity check | ||
expect((await this.erc721aLowCap.getOwnershipAt(offseted(4)[0]))[0]).to.equal(this.addr3.address); | ||
} | ||
}); | ||
|
||
describe('tokensOfOwner', async function () { | ||
it('returns the correct token ids', async function () { | ||
for (const minter of this.mintOrder) { | ||
const tokens = await this.erc721aLowCap.tokensOfOwner(minter.address); | ||
expect(tokens).to.eql(minter.expected.tokens); | ||
} | ||
}); | ||
|
||
it('returns the correct token ids after a transfer interferes with the normal logic', async function () { | ||
// Break sequential order by transfering 7th token from owner to addr4 | ||
const tokenIdToTransfer = offseted(7); | ||
await this.erc721aLowCap.transferFrom(this.owner.address, this.addr4.address, tokenIdToTransfer[0]); | ||
|
||
// Load balances | ||
const ownerTokens = await this.erc721aLowCap.tokensOfOwner(this.owner.address); | ||
const addr4Tokens = await this.erc721aLowCap.tokensOfOwner(this.addr4.address); | ||
|
||
// Verify the function can still read the correct token ids | ||
expect(ownerTokens).to.eql(offseted(6, 8)); | ||
expect(addr4Tokens).to.eql(tokenIdToTransfer); | ||
}); | ||
|
||
it('returns correct token ids with burned tokens', async function () { | ||
// Burn tokens | ||
const tokenIdToBurn = offseted(7); | ||
await this.erc721aLowCap.burn(tokenIdToBurn[0]); | ||
|
||
// Load balances | ||
const ownerTokens = await this.erc721aLowCap.tokensOfOwner(this.owner.address); | ||
|
||
// Verify the function can still read the correct token ids | ||
expect(ownerTokens).to.eql(offseted(6, 8)); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; | ||
|
||
describe('ERC721ALowCap', createTestSuite({ contract: 'ERC721ALowCapMock', constructorArgs: ['Azuki', 'AZUKI'] })); | ||
|
||
describe( | ||
'ERC721ALowCap override _startTokenId()', | ||
createTestSuite({ contract: 'ERC721ALowCapStartTokenIdMock', constructorArgs: ['Azuki', 'AZUKI', 1] }) | ||
); | ||
|
||
describe( | ||
'ERC721ALowCapOwnersExplicit', | ||
createTestSuite({ | ||
contract: 'ERC721ALowCapOwnersExplicitMock', | ||
constructorArgs: ['Azuki', 'AZUKI'], | ||
setOwnersExplicit: true, | ||
}) | ||
); |