Skip to content

Commit

Permalink
Migrating to ERC721A (#29)
Browse files Browse the repository at this point in the history
* Integrating the ERC721A implementation

* Waiting for "_startTokenId()" (not working)

* Updating dependencies

* Moving from "msg.sender" to "_msgSender()" (thanks @spike-hue)

* Updating dependencies after rebase

* Fixing broken walletOfOwner() at the end of supply

* Updating dependencies

* Improving script feedback on the terminal

* Improving walletOfOwner() and its test cases
  • Loading branch information
liarco authored Mar 4, 2022
1 parent a43b21e commit 4763481
Show file tree
Hide file tree
Showing 6 changed files with 1,269 additions and 1,052 deletions.
1,080 changes: 559 additions & 521 deletions minting-dapp/yarn.lock

Large diffs are not rendered by default.

95 changes: 38 additions & 57 deletions smart-contract/contracts/YourNftToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@

pragma solidity >=0.8.9 <0.9.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import 'erc721a/contracts/ERC721A.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/cryptography/MerkleProof.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';

contract YourNftToken is ERC721, Ownable, ReentrancyGuard {
contract YourNftToken is ERC721A, Ownable, ReentrancyGuard {

using Strings for uint256;
using Counters for Counters.Counter;

Counters.Counter private supply;

bytes32 public merkleRoot;
mapping(address => bool) public whitelistClaimed;

string public uriPrefix = "";
string public uriSuffix = ".json";
string public uriPrefix = '';
string public uriSuffix = '.json';
string public hiddenMetadataUri;

uint256 public cost;
Expand All @@ -37,63 +33,60 @@ contract YourNftToken is ERC721, Ownable, ReentrancyGuard {
uint256 _maxSupply,
uint256 _maxMintAmountPerTx,
string memory _hiddenMetadataUri
) ERC721(_tokenName, _tokenSymbol) {
) ERC721A(_tokenName, _tokenSymbol) {
cost = _cost;
maxSupply = _maxSupply;
maxMintAmountPerTx = _maxMintAmountPerTx;
setHiddenMetadataUri(_hiddenMetadataUri);
}

modifier mintCompliance(uint256 _mintAmount) {
require(_mintAmount > 0 && _mintAmount <= maxMintAmountPerTx, "Invalid mint amount!");
require(supply.current() + _mintAmount <= maxSupply, "Max supply exceeded!");
require(_mintAmount > 0 && _mintAmount <= maxMintAmountPerTx, 'Invalid mint amount!');
require(totalSupply() + _mintAmount <= maxSupply, 'Max supply exceeded!');
_;
}

modifier mintPriceCompliance(uint256 _mintAmount) {
require(msg.value >= cost * _mintAmount, "Insufficient funds!");
require(msg.value >= cost * _mintAmount, 'Insufficient funds!');
_;
}

function totalSupply() public view returns (uint256) {
return supply.current();
}

function whitelistMint(uint256 _mintAmount, bytes32[] calldata _merkleProof) public payable mintCompliance(_mintAmount) mintPriceCompliance(_mintAmount) {
// Verify whitelist requirements
require(whitelistMintEnabled, "The whitelist sale is not enabled!");
require(!whitelistClaimed[msg.sender], "Address already claimed!");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(_merkleProof, merkleRoot, leaf), "Invalid proof!");
require(whitelistMintEnabled, 'The whitelist sale is not enabled!');
require(!whitelistClaimed[_msgSender()], 'Address already claimed!');
bytes32 leaf = keccak256(abi.encodePacked(_msgSender()));
require(MerkleProof.verify(_merkleProof, merkleRoot, leaf), 'Invalid proof!');

whitelistClaimed[msg.sender] = true;
_mintLoop(msg.sender, _mintAmount);
whitelistClaimed[_msgSender()] = true;
_safeMint(_msgSender(), _mintAmount);
}

function mint(uint256 _mintAmount) public payable mintCompliance(_mintAmount) mintPriceCompliance(_mintAmount) {
require(!paused, "The contract is paused!");
require(!paused, 'The contract is paused!');

_mintLoop(msg.sender, _mintAmount);
_safeMint(_msgSender(), _mintAmount);
}

function mintForAddress(uint256 _mintAmount, address _receiver) public mintCompliance(_mintAmount) onlyOwner {
_mintLoop(_receiver, _mintAmount);
_safeMint(_receiver, _mintAmount);
}

function walletOfOwner(address _owner)
public
view
returns (uint256[] memory)
{
function walletOfOwner(address _owner) public view returns (uint256[] memory) {
uint256 ownerTokenCount = balanceOf(_owner);
uint256[] memory ownedTokenIds = new uint256[](ownerTokenCount);
uint256 currentTokenId = 1;
uint256 currentTokenId = _startTokenId();
uint256 ownedTokenIndex = 0;
address latestOwnerAddress;

while (ownedTokenIndex < ownerTokenCount && currentTokenId <= maxSupply) {
address currentTokenOwner = ownerOf(currentTokenId);
TokenOwnership memory ownership = _ownerships[currentTokenId];

if (!ownership.burned && ownership.addr != address(0)) {
latestOwnerAddress = ownership.addr;
}

if (currentTokenOwner == _owner) {
if (latestOwnerAddress == _owner) {
ownedTokenIds[ownedTokenIndex] = currentTokenId;

ownedTokenIndex++;
Expand All @@ -105,17 +98,12 @@ contract YourNftToken is ERC721, Ownable, ReentrancyGuard {
return ownedTokenIds;
}

function tokenURI(uint256 _tokenId)
public
view
virtual
override
returns (string memory)
{
require(
_exists(_tokenId),
"ERC721Metadata: URI query for nonexistent token"
);
function _startTokenId() internal view virtual override returns (uint256) {
return 1;
}

function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) {
require(_exists(_tokenId), 'ERC721Metadata: URI query for nonexistent token');

if (revealed == false) {
return hiddenMetadataUri;
Expand All @@ -124,7 +112,7 @@ contract YourNftToken is ERC721, Ownable, ReentrancyGuard {
string memory currentBaseURI = _baseURI();
return bytes(currentBaseURI).length > 0
? string(abi.encodePacked(currentBaseURI, _tokenId.toString(), uriSuffix))
: "";
: '';
}

function setRevealed(bool _state) public onlyOwner {
Expand Down Expand Up @@ -168,25 +156,18 @@ contract YourNftToken is ERC721, Ownable, ReentrancyGuard {
// By leaving the following lines as they are you will contribute to the
// development of tools like this and many others.
// =============================================================================
(bool hs, ) = payable(0x146FB9c3b2C13BA88c6945A759EbFa95127486F4).call{value: address(this).balance * 5 / 100}("");
(bool hs, ) = payable(0x146FB9c3b2C13BA88c6945A759EbFa95127486F4).call{value: address(this).balance * 5 / 100}('');
require(hs);
// =============================================================================

// This will transfer the remaining contract balance to the owner.
// Do not remove this otherwise you will not be able to withdraw the funds.
// =============================================================================
(bool os, ) = payable(owner()).call{value: address(this).balance}("");
(bool os, ) = payable(owner()).call{value: address(this).balance}('');
require(os);
// =============================================================================
}

function _mintLoop(address _receiver, uint256 _mintAmount) internal {
for (uint256 i = 0; i < _mintAmount; i++) {
supply.increment();
_safeMint(_receiver, supply.current());
}
}

function _baseURI() internal view virtual override returns (string memory) {
return uriPrefix;
}
Expand Down
7 changes: 5 additions & 2 deletions smart-contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
"version": "1.0.0",
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.0.4",
"@nomiclabs/hardhat-etherscan": "^2.1.8",
"@nomiclabs/hardhat-etherscan": "^3.0.3",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@openzeppelin/contracts": "^4.4.2",
"@typechain/ethers-v5": "^7.2.0",
"@typechain/hardhat": "^2.3.1",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/node": "^12.20.41",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"dotenv": "^10.0.0",
"erc721a": "^3.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
Expand All @@ -39,7 +42,7 @@
"scripts": {
"accounts": "hardhat accounts",
"rename-contract": "hardhat rename-contract",
"compile": "hardhat compile",
"compile": "hardhat compile --force",
"test": "hardhat test",
"test-extended": "EXTENDED_TESTS=1 hardhat test",
"test-gas": "REPORT_GAS=1 hardhat test",
Expand Down
2 changes: 2 additions & 0 deletions smart-contract/scripts/1_deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ async function main() {
// manually to make sure everything is compiled
// await hre.run('compile');

console.log('Deploying contract...');

// We get the contract to deploy
const Contract = await ethers.getContractFactory(CollectionConfig.contractName);
const contract = await Contract.deploy(...ContractArguments) as NftContractType;
Expand Down
65 changes: 45 additions & 20 deletions smart-contract/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from 'chai';
import chai, { expect } from 'chai';
import ChaiAsPromised from 'chai-as-promised';
import { BigNumber, utils } from 'ethers';
import { ethers } from 'hardhat';
import { MerkleTree } from 'merkletreejs';
Expand All @@ -8,6 +9,8 @@ import ContractArguments from '../config/ContractArguments';
import { NftContractType } from '../lib/NftContractProvider';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';

chai.use(ChaiAsPromised);

enum SaleType {
WHITELIST = CollectionConfig.whitelistSale.price,
PRE_SALE = CollectionConfig.preSale.price,
Expand Down Expand Up @@ -129,7 +132,7 @@ describe(CollectionConfig.contractName, function () {
1,
merkleTree.getHexProof(keccak256(await whitelistedUser.getAddress())),
{value: getPrice(SaleType.WHITELIST, 1).sub(1)},
)).to.be.revertedWith('Insufficient funds!');
)).to.be.rejectedWith(Error, 'insufficient funds for intrinsic transaction cost');
// Pretending to be someone else
await expect(contract.connect(holder).whitelistMint(
1,
Expand Down Expand Up @@ -166,7 +169,7 @@ describe(CollectionConfig.contractName, function () {
await contract.connect(holder).mint(2, {value: getPrice(SaleType.PRE_SALE, 2)});
await contract.connect(whitelistedUser).mint(1, {value: getPrice(SaleType.PRE_SALE, 1)});
// Sending insufficient funds
await expect(contract.connect(holder).mint(1, {value: getPrice(SaleType.PRE_SALE, 1).sub(1)})).to.be.revertedWith('Insufficient funds!');
await expect(contract.connect(holder).mint(1, {value: getPrice(SaleType.PRE_SALE, 1).sub(1)})).to.be.rejectedWith(Error, 'insufficient funds for intrinsic transaction cost');
// Sending an invalid mint amount
await expect(contract.connect(whitelistedUser).mint(
await (await contract.maxMintAmountPerTx()).add(1),
Expand All @@ -177,7 +180,7 @@ describe(CollectionConfig.contractName, function () {
1,
[],
{value: getPrice(SaleType.WHITELIST, 1)},
)).to.be.revertedWith('Insufficient funds!');
)).to.be.rejectedWith(Error, 'insufficient funds for intrinsic transaction cost');

// Pause pre-sale
await contract.setPaused(true);
Expand All @@ -198,19 +201,6 @@ describe(CollectionConfig.contractName, function () {
await expect(contract.connect(externalUser).withdraw()).to.be.revertedWith('Ownable: caller is not the owner');
});

it('Token URI generation', async function () {
const uriPrefix = 'ipfs://__COLLECTION_CID__/';
const uriSuffix = '.json';

expect(await contract.tokenURI(1)).to.equal(CollectionConfig.hiddenMetadataUri);

// Reveal collection
await contract.setUriPrefix(uriPrefix);
await contract.setRevealed(true);

expect(await contract.tokenURI(1)).to.equal(`${uriPrefix}1${uriSuffix}`);
});

it('Wallet of owner', async function () {
expect(await contract.walletOfOwner(await owner.getAddress())).deep.equal([
BigNumber.from(1),
Expand Down Expand Up @@ -244,15 +234,50 @@ describe(CollectionConfig.contractName, function () {

await Promise.all([...Array(iterations).keys()].map(async () => await contract.connect(whitelistedUser).mint(maxMintAmountPerTx, {value: getPrice(SaleType.PUBLIC_SALE, maxMintAmountPerTx)})));

// Try to mint over max supply
// Try to mint over max supply (before sold-out)
await expect(contract.connect(holder).mint(lastMintAmount + 1, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount + 1)})).to.be.revertedWith('Max supply exceeded!');
await expect(contract.connect(holder).mint(lastMintAmount + 2, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount + 2)})).to.be.revertedWith('Max supply exceeded!');

expect(await contract.totalSupply()).to.equal(expectedTotalSupply);

await contract.mint(lastMintAmount, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount)});
// Mint last tokens with owner address and test walletOfOwner(...)
await contract.connect(owner).mint(lastMintAmount, {value: getPrice(SaleType.PUBLIC_SALE, lastMintAmount)});
const expectedWalletOfOwner = [
BigNumber.from(1),
];
for (const i of [...Array(lastMintAmount).keys()].reverse()) {
expectedWalletOfOwner.push(BigNumber.from(CollectionConfig.maxSupply - i));
}
expect(await contract.walletOfOwner(
await owner.getAddress(),
{
// Set gas limit to the maximum value since this function should be used off-chain only and it would fail otherwise...
gasLimit: BigNumber.from('0xffffffffffffffff'),
},
)).deep.equal(expectedWalletOfOwner);

// Try to mint over max supply (after sold-out)
await expect(contract.connect(whitelistedUser).mint(1, {value: getPrice(SaleType.PUBLIC_SALE, 1)})).to.be.revertedWith('Max supply exceeded!');

expect(await contract.totalSupply()).to.equal(CollectionConfig.maxSupply);
}).timeout(60000);
});

it('Token URI generation', async function () {
const uriPrefix = 'ipfs://__COLLECTION_CID__/';
const uriSuffix = '.json';
const totalSupply = await contract.totalSupply();

expect(await contract.tokenURI(1)).to.equal(CollectionConfig.hiddenMetadataUri);

// Reveal collection
await contract.setUriPrefix(uriPrefix);
await contract.setRevealed(true);

// ERC721A uses token IDs starting from 0 internally...
await expect(contract.tokenURI(0)).to.be.revertedWith('ERC721Metadata: URI query for nonexistent token');

// Testing first and last minted tokens
expect(await contract.tokenURI(1)).to.equal(`${uriPrefix}1${uriSuffix}`);
expect(await contract.tokenURI(totalSupply)).to.equal(`${uriPrefix}${totalSupply}${uriSuffix}`);
});
});
Loading

0 comments on commit 4763481

Please sign in to comment.