From 120e150aa2b33b50950155ae70c36a0c5ec1a567 Mon Sep 17 00:00:00 2001 From: Channing Date: Tue, 12 Mar 2024 14:19:53 -0700 Subject: [PATCH 1/6] Support variable wallet limit --- contracts/ERC721CM.sol | 24 ++++++++++++-- contracts/ERC721M.sol | 31 +++++++++++++++++-- contracts/ERC721MAutoApprover.sol | 2 +- .../ERC721MOperatorFiltererAutoApprover.sol | 2 +- contracts/IERC721M.sol | 9 +++++- contracts/onft/ERC721MLite.sol | 24 +++++++++++++- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/contracts/ERC721CM.sol b/contracts/ERC721CM.sol index a8980bd7..53cad3a8 100644 --- a/contracts/ERC721CM.sol +++ b/contracts/ERC721CM.sol @@ -367,7 +367,26 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard { uint64 timestamp, bytes calldata signature ) external payable virtual nonReentrant { - _mintInternal(qty, msg.sender, proof, timestamp, signature); + _mintInternal(qty, msg.sender, 0, proof, timestamp, signature); + } + + /** + * @dev Mints token(s) with limit. + * + * qty - number of tokens to mint + * limit - limit for the given minter + * proof - the merkle proof generated on client side. This applies if using whitelist. + * timestamp - the current timestamp + * signature - the signature from cosigner if using cosigner. + */ + function mintWithLimit( + uint32 qty, + uint32 limit, + bytes32[] calldata proof, + uint64 timestamp, + bytes calldata signature + ) external payable virtual nonReentrant { + _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); } /** @@ -391,7 +410,7 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard { // Check the caller is Crossmint if (msg.sender != _crossmintAddress) revert CrossmintOnly(); - _mintInternal(qty, to, proof, timestamp, signature); + _mintInternal(qty, to, 0, proof, timestamp, signature); } /** @@ -400,6 +419,7 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard { function _mintInternal( uint32 qty, address to, + uint32 limit, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature diff --git a/contracts/ERC721M.sol b/contracts/ERC721M.sol index 9468e628..8b7a4288 100644 --- a/contracts/ERC721M.sol +++ b/contracts/ERC721M.sol @@ -363,7 +363,26 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { uint64 timestamp, bytes calldata signature ) external payable virtual nonReentrant { - _mintInternal(qty, msg.sender, proof, timestamp, signature); + _mintInternal(qty, msg.sender, 0, proof, timestamp, signature); + } + + /** + * @dev Mints token(s) with limit. + * + * qty - number of tokens to mint + * limit - limit for the given minter + * proof - the merkle proof generated on client side. This applies if using whitelist. + * timestamp - the current timestamp + * signature - the signature from cosigner if using cosigner. + */ + function mintWithLimit( + uint32 qty, + uint32 limit, + bytes32[] calldata proof, + uint64 timestamp, + bytes calldata signature + ) external payable virtual nonReentrant { + _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); } /** @@ -387,7 +406,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { // Check the caller is Crossmint if (msg.sender != _crossmintAddress) revert CrossmintOnly(); - _mintInternal(qty, to, proof, timestamp, signature); + _mintInternal(qty, to, 0, proof, timestamp, signature); } /** @@ -396,6 +415,7 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { function _mintInternal( uint32 qty, address to, + uint32 limit, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature @@ -442,9 +462,14 @@ contract ERC721M is IERC721M, ERC721AQueryable, Ownable, ReentrancyGuard { if ( MerkleProof.processProof( proof, - keccak256(abi.encodePacked(to)) + keccak256(abi.encodePacked(to, limit)) ) != stage.merkleRoot ) revert InvalidProof(); + + // Verify merkle proof mint limit + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + revert WalletStageLimitExceeded(); + } } if (_mintCurrency != address(0)) { diff --git a/contracts/ERC721MAutoApprover.sol b/contracts/ERC721MAutoApprover.sol index 40081273..cb4f06da 100644 --- a/contracts/ERC721MAutoApprover.sol +++ b/contracts/ERC721MAutoApprover.sol @@ -40,7 +40,7 @@ contract ERC721MAutoApprover is ERC721M { uint64 timestamp, bytes calldata signature ) external payable override nonReentrant { - _mintInternal(qty, msg.sender, proof, timestamp, signature); + _mintInternal(qty, msg.sender, 0, proof, timestamp, signature); // if auto approve address is not all zero, check if the address is already approved if ( diff --git a/contracts/ERC721MOperatorFiltererAutoApprover.sol b/contracts/ERC721MOperatorFiltererAutoApprover.sol index abef74a4..6e6a8ce9 100644 --- a/contracts/ERC721MOperatorFiltererAutoApprover.sol +++ b/contracts/ERC721MOperatorFiltererAutoApprover.sol @@ -40,7 +40,7 @@ contract ERC721MOperatorFiltererAutoApprover is ERC721MOperatorFilterer { uint64 timestamp, bytes calldata signature ) external payable override nonReentrant { - _mintInternal(qty, msg.sender, proof, timestamp, signature); + _mintInternal(qty, msg.sender, 0, proof, timestamp, signature); // if auto approve address is not all zero, check if the address is already approved if ( diff --git a/contracts/IERC721M.sol b/contracts/IERC721M.sol index 96942c19..0d574573 100644 --- a/contracts/IERC721M.sol +++ b/contracts/IERC721M.sol @@ -26,6 +26,7 @@ interface IERC721M is IERC721AQueryable { error WalletStageLimitExceeded(); error WithdrawFailed(); error WrongMintCurrency(); + error NotSupported(); struct MintStageInfo { uint80 price; @@ -75,4 +76,10 @@ interface IERC721M is IERC721AQueryable { uint32, uint256 ); -} + + function mint(uint32 qty, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable; + + function mintWithLimit(uint32 qty, uint32 limit, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable; + + function crossmint(uint32 qty, address to, bytes32[] calldata proof, uint64 timestamp, bytes calldata signature) external payable; +} \ No newline at end of file diff --git a/contracts/onft/ERC721MLite.sol b/contracts/onft/ERC721MLite.sol index d9d21b62..68d90b7c 100644 --- a/contracts/onft/ERC721MLite.sol +++ b/contracts/onft/ERC721MLite.sol @@ -307,7 +307,7 @@ contract ERC721MLite is if ( MerkleProof.processProof( proof, - keccak256(abi.encodePacked(msg.sender)) + keccak256(abi.encodePacked(msg.sender, uint32(0))) // limit = 0 for consistency ) != stage.merkleRoot ) revert InvalidProof(); } @@ -317,6 +317,28 @@ contract ERC721MLite is _safeMint(msg.sender, qty); } + /** NOT SUPPORTED */ + function mintWithLimit( + uint32 qty, + uint32 limit, + bytes32[] calldata proof, + uint64 timestamp, + bytes calldata signature + ) external payable virtual nonReentrant { + revert NotSupported(); + } + + /** NOT SUPPORTED */ + function crossmint( + uint32 qty, + address to, + bytes32[] calldata proof, + uint64 timestamp, + bytes calldata signature + ) external payable nonReentrant { + revert NotSupported(); + } + /** * @dev Mints token(s) by owner. * From 17d0e661d32c594310c9f0b1a7b33a8504f5d946 Mon Sep 17 00:00:00 2001 From: Channing Date: Tue, 12 Mar 2024 18:06:23 -0700 Subject: [PATCH 2/6] Support variable wallet limit --- contracts/ERC721CM.sol | 7 ++- test/ERC721CM.test.ts | 115 +++++++++++++++++++++++++++++++++++++---- test/erc721m.test.ts | 104 +++++++++++++++++++++++++++++++++++-- 3 files changed, 209 insertions(+), 17 deletions(-) diff --git a/contracts/ERC721CM.sol b/contracts/ERC721CM.sol index 53cad3a8..255a7e22 100644 --- a/contracts/ERC721CM.sol +++ b/contracts/ERC721CM.sol @@ -466,9 +466,14 @@ contract ERC721CM is IERC721M, ERC721ACQueryable, Ownable, ReentrancyGuard { if ( MerkleProof.processProof( proof, - keccak256(abi.encodePacked(to)) + keccak256(abi.encodePacked(to, limit)) ) != stage.merkleRoot ) revert InvalidProof(); + + // Verify merkle proof mint limit + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + revert WalletStageLimitExceeded(); + } } if (_mintCurrency != address(0)) { diff --git a/test/ERC721CM.test.ts b/test/ERC721CM.test.ts index d1809446..8d2a790a 100644 --- a/test/ERC721CM.test.ts +++ b/test/ERC721CM.test.ts @@ -4,9 +4,8 @@ import chaiAsPromised from 'chai-as-promised'; import { ethers } from 'hardhat'; import { MerkleTree } from 'merkletreejs'; import { ERC721CM } from '../typechain-types'; -import { setMintable } from '../scripts/setMintable'; -const { keccak256, getAddress } = ethers.utils; +const { getAddress } = ethers.utils; chai.use(chaiAsPromised); @@ -1197,16 +1196,19 @@ describe('ERC721CM', function () { it('enforces Merkle proof if required', async () => { const accounts = (await ethers.getSigners()).map((signer) => - getAddress(signer.address), + getAddress(signer.address).toLowerCase().trim(), ); + const leaves = accounts.map(account => + ethers.utils.solidityKeccak256(['address', 'uint32'], [account, 0])); const signerAddress = await ethers.provider.getSigner().getAddress(); - - const merkleTree = new MerkleTree(accounts, keccak256, { + const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { sortPairs: true, - hashLeaves: true, + hashLeaves: false, }); const root = merkleTree.getHexRoot(); - const proof = merkleTree.getHexProof(keccak256(signerAddress)); + + const leaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [signerAddress.toLowerCase().trim(), 0]); + const proof = merkleTree.getHexProof(leaf); const block = await ethers.provider.getBlock( await ethers.provider.getBlockNumber(), @@ -1273,6 +1275,97 @@ describe('ERC721CM', function () { await expect(mint).to.be.revertedWith('InvalidProof'); }); + it('mint with limit', async () => { + const ownerAddress = await owner.getAddress(); + const readerAddress = await readonly.getAddress() + const leaves = [ + ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]), + ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]) + ]; + + const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { + sortPairs: true, + hashLeaves: false, + }); + const root = merkleTree.getHexRoot(); + const ownerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]); + const readerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]); + const ownerProof = merkleTree.getHexProof(ownerLeaf); + const readerProof = merkleTree.getHexProof(readerLeaf); + + const block = await ethers.provider.getBlock( + await ethers.provider.getBlockNumber(), + ); + // +10 is a number bigger than the count of transactions up to mint + const stageStart = block.timestamp + 10; + // Set stages + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 10, + merkleRoot: root, + maxStageSupply: 100, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 100, + }, + ]); + await contract.setMintable(true); + + // Setup the test context: Update block.timestamp to comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + // Owner mints 1 token with valid proof + await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }); + expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(1); + + // Owner mints 1 token with wrong limit and should be reverted. + await expect( + contract.mintWithLimit(1, 3, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }), + ).to.be.rejectedWith('InvalidProof'); + + // Owner mints 2 tokens with valid proof and reverts. + await expect( + contract.mintWithLimit(2, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('1.0'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Owner mints 1 token with valid proof. Now owner reaches the limit. + await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }); + expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(2); + + // Owner tries to mint more and reverts. + await expect( + contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Reader mints 6 tokens with valid proof and reverts. + await expect( + readonlyContract.mintWithLimit(6, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('3.0'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Reader mints 5 tokens with valid proof. + await readonlyContract.mintWithLimit(5, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('2.5'), + }); + + // Reader mints 1 token with valid proof and reverts. + await expect( + readonlyContract.mintWithLimit(1, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + }); + it('crossmint', async () => { const crossmintAddressStr = '0xdAb1a1854214684acE522439684a145E62505233'; const ERC721CM = await ethers.getContractFactory('ERC721CM'); @@ -1425,12 +1518,12 @@ describe('ERC721CM', function () { ); const [_, recipient] = await ethers.getSigners(); - const merkleTree = new MerkleTree(accounts, keccak256, { + const merkleTree = new MerkleTree(accounts, ethers.utils.keccak256, { sortPairs: true, hashLeaves: true, }); const root = merkleTree.getHexRoot(); - const proof = merkleTree.getHexProof(keccak256(recipient.address)); + const proof = merkleTree.getHexProof(ethers.utils.keccak256(recipient.address)); await contract.setStages([ { @@ -1464,12 +1557,12 @@ describe('ERC721CM', function () { ); const [_, recipient] = await ethers.getSigners(); - const merkleTree = new MerkleTree(accounts, keccak256, { + const merkleTree = new MerkleTree(accounts, ethers.utils.keccak256, { sortPairs: true, hashLeaves: true, }); const root = merkleTree.getHexRoot(); - const proof = merkleTree.getHexProof(keccak256(recipient.address)); + const proof = merkleTree.getHexProof(ethers.utils.keccak256(recipient.address)); await contract.setStages([ { diff --git a/test/erc721m.test.ts b/test/erc721m.test.ts index c4838bff..9672ebbe 100644 --- a/test/erc721m.test.ts +++ b/test/erc721m.test.ts @@ -1198,16 +1198,19 @@ describe('ERC721M', function () { it('enforces Merkle proof if required', async () => { const accounts = (await ethers.getSigners()).map((signer) => - getAddress(signer.address), + getAddress(signer.address).toLowerCase().trim(), ); + const leaves = accounts.map(account => + ethers.utils.solidityKeccak256(['address', 'uint32'], [account, 0])); const signerAddress = await ethers.provider.getSigner().getAddress(); - - const merkleTree = new MerkleTree(accounts, keccak256, { + const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { sortPairs: true, - hashLeaves: true, + hashLeaves: false, }); const root = merkleTree.getHexRoot(); - const proof = merkleTree.getHexProof(keccak256(signerAddress)); + + const leaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [signerAddress.toLowerCase().trim(), 0]); + const proof = merkleTree.getHexProof(leaf); const block = await ethers.provider.getBlock( await ethers.provider.getBlockNumber(), @@ -1274,6 +1277,97 @@ describe('ERC721M', function () { await expect(mint).to.be.revertedWith('InvalidProof'); }); + it('mint with limit', async () => { + const ownerAddress = await owner.getAddress(); + const readerAddress = await readonly.getAddress() + const leaves = [ + ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]), + ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]) + ]; + + const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { + sortPairs: true, + hashLeaves: false, + }); + const root = merkleTree.getHexRoot(); + const ownerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]); + const readerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]); + const ownerProof = merkleTree.getHexProof(ownerLeaf); + const readerProof = merkleTree.getHexProof(readerLeaf); + + const block = await ethers.provider.getBlock( + await ethers.provider.getBlockNumber(), + ); + // +10 is a number bigger than the count of transactions up to mint + const stageStart = block.timestamp + 10; + // Set stages + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 10, + merkleRoot: root, + maxStageSupply: 100, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 100, + }, + ]); + await contract.setMintable(true); + + // Setup the test context: Update block.timestamp to comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + // Owner mints 1 token with valid proof + await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }); + expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(1); + + // Owner mints 1 token with wrong limit and should be reverted. + await expect( + contract.mintWithLimit(1, 3, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }), + ).to.be.rejectedWith('InvalidProof'); + + // Owner mints 2 tokens with valid proof and reverts. + await expect( + contract.mintWithLimit(2, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('1.0'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Owner mints 1 token with valid proof. Now owner reaches the limit. + await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }); + expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(2); + + // Owner tries to mint more and reverts. + await expect( + contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Reader mints 6 tokens with valid proof and reverts. + await expect( + readonlyContract.mintWithLimit(6, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('3.0'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Reader mints 5 tokens with valid proof. + await readonlyContract.mintWithLimit(5, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('2.5'), + }); + + // Reader mints 1 token with valid proof and reverts. + await expect( + readonlyContract.mintWithLimit(1, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + }); + it('crossmint', async () => { const crossmintAddressStr = '0xdAb1a1854214684acE522439684a145E62505233'; const ERC721M = await ethers.getContractFactory('ERC721M'); From f5cab4a6208049599b9179508c9b398d5552434b Mon Sep 17 00:00:00 2001 From: Channing Date: Wed, 13 Mar 2024 15:31:36 -0700 Subject: [PATCH 3/6] Update scripts --- scripts/setStages.ts | 78 ++++++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/scripts/setStages.ts b/scripts/setStages.ts index cd92b9b0..e3a99a48 100644 --- a/scripts/setStages.ts +++ b/scripts/setStages.ts @@ -20,6 +20,7 @@ interface StageConfig { walletLimit?: number; maxSupply?: number; whitelistPath?: string; + variableWalletLimitPath?: string; } export const setStages = async ( @@ -41,35 +42,62 @@ export const setStages = async ( if (args.gaslimit) { overrides.gasLimit = ethers.BigNumber.from(args.gaslimit); } + const merkleRoots = await Promise.all( stagesConfig.map((stage) => { - if (!stage.whitelistPath) { - return ethers.utils.hexZeroPad('0x', 32); - } - const whitelist = JSON.parse( - fs.readFileSync(stage.whitelistPath, 'utf-8'), - ); - - // Clean up whitelist - const filteredWhitelist= whitelist.filter((address: string) => ethers.utils.isAddress(address)); - console.log(`Filtered whitelist: ${filteredWhitelist.length} addresses. ${whitelist.length - filteredWhitelist.length} invalid addresses removed.`); - const invalidWhitelist= whitelist.filter((address: string) => !ethers.utils.isAddress(address)); - console.log(`❌ Invalid whitelist: ${invalidWhitelist.length} addresses.\r\n${invalidWhitelist.join(', \r\n')}`); - - if (invalidWhitelist.length > 0) { - console.log(`🔄 🚨 updating whitelist file: ${stage.whitelistPath}`); - fs.writeFileSync(stage.whitelistPath, JSON.stringify(filteredWhitelist, null, 2)) - } + if (stage.whitelistPath) { + const whitelist = JSON.parse( + fs.readFileSync(stage.whitelistPath, 'utf-8'), + ); + + // Clean up whitelist + const filteredWhitelist= whitelist.filter((address: string) => ethers.utils.isAddress(address)); + console.log(`Filtered whitelist: ${filteredWhitelist.length} addresses. ${whitelist.length - filteredWhitelist.length} invalid addresses removed.`); + const invalidWhitelist= whitelist.filter((address: string) => !ethers.utils.isAddress(address)); + console.log(`❌ Invalid whitelist: ${invalidWhitelist.length} addresses.\r\n${invalidWhitelist.join(', \r\n')}`); + + if (invalidWhitelist.length > 0) { + console.log(`🔄 🚨 updating whitelist file: ${stage.whitelistPath}`); + fs.writeFileSync(stage.whitelistPath, JSON.stringify(filteredWhitelist, null, 2)) + } - const mt = new MerkleTree( - filteredWhitelist.map(ethers.utils.getAddress), - ethers.utils.keccak256, - { + const mt = new MerkleTree( + filteredWhitelist.map(ethers.utils.getAddress), + ethers.utils.keccak256, + { + sortPairs: true, + hashLeaves: true, + }, + ); + return mt.getHexRoot(); + } else if (stage.variableWalletLimitPath) { + const leaves: any[] = []; + const file = fs.readFileSync(stage.variableWalletLimitPath, 'utf-8'); + file + .split('\n') + .filter((line) => line) + .forEach((line) => { + const [addressStr, limitStr] = line.split(','); + const limit = parseInt(limitStr, 10); + + if (!ethers.utils.isAddress(addressStr.trim().toLowerCase()) && limit < 0) { + console.log(`Filtered address: ${addressStr}`); + return; + } + + const address = ethers.utils.getAddress(addressStr.trim().toLowerCase()); + const digest = ethers.utils.solidityKeccak256(['address', 'uint32'], [address, limit]); + leaves.push(digest); + }); + + const mt = new MerkleTree(leaves, ethers.utils.keccak256, { sortPairs: true, - hashLeaves: true, - }, - ); - return mt.getHexRoot(); + hashLeaves: false, + }); + return mt.getHexRoot(); + } + + return ethers.utils.hexZeroPad('0x', 32); }), ); From 3e43c473aeae486376771c359cbc0824b61ba924 Mon Sep 17 00:00:00 2001 From: Channing Date: Wed, 13 Mar 2024 15:45:30 -0700 Subject: [PATCH 4/6] update --- scripts/setStages.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/setStages.ts b/scripts/setStages.ts index e3a99a48..9182c5dc 100644 --- a/scripts/setStages.ts +++ b/scripts/setStages.ts @@ -78,14 +78,20 @@ export const setStages = async ( .filter((line) => line) .forEach((line) => { const [addressStr, limitStr] = line.split(','); - const limit = parseInt(limitStr, 10); - if (!ethers.utils.isAddress(addressStr.trim().toLowerCase()) && limit < 0) { - console.log(`Filtered address: ${addressStr}`); + if (!ethers.utils.isAddress(addressStr.trim().toLowerCase())) { + console.log(`Ignored invalid address: ${addressStr}`); return; } const address = ethers.utils.getAddress(addressStr.trim().toLowerCase()); + const limit = parseInt(limitStr, 10); + + if (!Number.isInteger(limit)) { + console.log(`Ignored invalid limit for address: ${addressStr}`); + return; + } + const digest = ethers.utils.solidityKeccak256(['address', 'uint32'], [address, limit]); leaves.push(digest); }); From 27185997f54f67a921ad483a8bccbc1712e8a716 Mon Sep 17 00:00:00 2001 From: Channing Date: Wed, 13 Mar 2024 17:17:15 -0700 Subject: [PATCH 5/6] fix script --- scripts/setStages.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/setStages.ts b/scripts/setStages.ts index 9182c5dc..2501c765 100644 --- a/scripts/setStages.ts +++ b/scripts/setStages.ts @@ -43,6 +43,11 @@ export const setStages = async ( overrides.gasLimit = ethers.BigNumber.from(args.gaslimit); } + /* + * Merkle root generation logic: + * - for `whitelist`, leaves are `solidityKeccak256(['address', 'uint32'], [address, 0])` + * - for `variable wallet limit list`, leaves are `solidityKeccak256(['address', 'uint32'], [address, limit])` + */ const merkleRoots = await Promise.all( stagesConfig.map((stage) => { if (stage.whitelistPath) { @@ -62,7 +67,7 @@ export const setStages = async ( } const mt = new MerkleTree( - filteredWhitelist.map(ethers.utils.getAddress), + filteredWhitelist.map((address: string) => ethers.utils.solidityKeccak256(['address', 'uint32'], [ethers.utils.getAddress(address), 0])), ethers.utils.keccak256, { sortPairs: true, From 3d143c563fe0242c9cd818e4ea633a1c3cc55880 Mon Sep 17 00:00:00 2001 From: Channing Date: Wed, 13 Mar 2024 23:50:48 -0700 Subject: [PATCH 6/6] lint --- test/ERC721CM.test.ts | 50 ++++++++++++++++++++++++++++++++----------- test/erc721m.test.ts | 42 ++++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/test/ERC721CM.test.ts b/test/ERC721CM.test.ts index 8a4e3c85..2b848eda 100644 --- a/test/ERC721CM.test.ts +++ b/test/ERC721CM.test.ts @@ -1198,8 +1198,9 @@ describe('ERC721CM', function () { const accounts = (await ethers.getSigners()).map((signer) => getAddress(signer.address).toLowerCase().trim(), ); - const leaves = accounts.map(account => - ethers.utils.solidityKeccak256(['address', 'uint32'], [account, 0])); + const leaves = accounts.map((account) => + ethers.utils.solidityKeccak256(['address', 'uint32'], [account, 0]), + ); const signerAddress = await ethers.provider.getSigner().getAddress(); const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { sortPairs: true, @@ -1207,7 +1208,10 @@ describe('ERC721CM', function () { }); const root = merkleTree.getHexRoot(); - const leaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [signerAddress.toLowerCase().trim(), 0]); + const leaf = ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [signerAddress.toLowerCase().trim(), 0], + ); const proof = merkleTree.getHexProof(leaf); const block = await ethers.provider.getBlock( @@ -1277,10 +1281,16 @@ describe('ERC721CM', function () { it('mint with limit', async () => { const ownerAddress = await owner.getAddress(); - const readerAddress = await readonly.getAddress() + const readerAddress = await readonly.getAddress(); const leaves = [ - ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]), - ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]) + ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [ownerAddress, 2], + ), + ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [readerAddress, 5], + ), ]; const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { @@ -1288,8 +1298,14 @@ describe('ERC721CM', function () { hashLeaves: false, }); const root = merkleTree.getHexRoot(); - const ownerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]); - const readerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]); + const ownerLeaf = ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [ownerAddress, 2], + ); + const readerLeaf = ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [readerAddress, 5], + ); const ownerProof = merkleTree.getHexProof(ownerLeaf); const readerProof = merkleTree.getHexProof(readerLeaf); @@ -1317,7 +1333,9 @@ describe('ERC721CM', function () { await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { value: ethers.utils.parseEther('0.5'), }); - expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(1); + expect( + (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), + ).to.equal(1); // Owner mints 1 token with wrong limit and should be reverted. await expect( @@ -1332,12 +1350,14 @@ describe('ERC721CM', function () { value: ethers.utils.parseEther('1.0'), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); - + // Owner mints 1 token with valid proof. Now owner reaches the limit. await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { value: ethers.utils.parseEther('0.5'), }); - expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(2); + expect( + (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), + ).to.equal(2); // Owner tries to mint more and reverts. await expect( @@ -1521,7 +1541,9 @@ describe('ERC721CM', function () { hashLeaves: true, }); const root = merkleTree.getHexRoot(); - const proof = merkleTree.getHexProof(ethers.utils.keccak256(recipient.address)); + const proof = merkleTree.getHexProof( + ethers.utils.keccak256(recipient.address), + ); await contract.setStages([ { @@ -1560,7 +1582,9 @@ describe('ERC721CM', function () { hashLeaves: true, }); const root = merkleTree.getHexRoot(); - const proof = merkleTree.getHexProof(ethers.utils.keccak256(recipient.address)); + const proof = merkleTree.getHexProof( + ethers.utils.keccak256(recipient.address), + ); await contract.setStages([ { diff --git a/test/erc721m.test.ts b/test/erc721m.test.ts index 72388841..b46f0e02 100644 --- a/test/erc721m.test.ts +++ b/test/erc721m.test.ts @@ -1200,8 +1200,9 @@ describe('ERC721M', function () { const accounts = (await ethers.getSigners()).map((signer) => getAddress(signer.address).toLowerCase().trim(), ); - const leaves = accounts.map(account => - ethers.utils.solidityKeccak256(['address', 'uint32'], [account, 0])); + const leaves = accounts.map((account) => + ethers.utils.solidityKeccak256(['address', 'uint32'], [account, 0]), + ); const signerAddress = await ethers.provider.getSigner().getAddress(); const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { sortPairs: true, @@ -1209,7 +1210,10 @@ describe('ERC721M', function () { }); const root = merkleTree.getHexRoot(); - const leaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [signerAddress.toLowerCase().trim(), 0]); + const leaf = ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [signerAddress.toLowerCase().trim(), 0], + ); const proof = merkleTree.getHexProof(leaf); const block = await ethers.provider.getBlock( @@ -1279,10 +1283,16 @@ describe('ERC721M', function () { it('mint with limit', async () => { const ownerAddress = await owner.getAddress(); - const readerAddress = await readonly.getAddress() + const readerAddress = await readonly.getAddress(); const leaves = [ - ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]), - ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]) + ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [ownerAddress, 2], + ), + ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [readerAddress, 5], + ), ]; const merkleTree = new MerkleTree(leaves, ethers.utils.keccak256, { @@ -1290,8 +1300,14 @@ describe('ERC721M', function () { hashLeaves: false, }); const root = merkleTree.getHexRoot(); - const ownerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [ownerAddress, 2]); - const readerLeaf = ethers.utils.solidityKeccak256(['address', 'uint32'], [readerAddress, 5]); + const ownerLeaf = ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [ownerAddress, 2], + ); + const readerLeaf = ethers.utils.solidityKeccak256( + ['address', 'uint32'], + [readerAddress, 5], + ); const ownerProof = merkleTree.getHexProof(ownerLeaf); const readerProof = merkleTree.getHexProof(readerLeaf); @@ -1319,7 +1335,9 @@ describe('ERC721M', function () { await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { value: ethers.utils.parseEther('0.5'), }); - expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(1); + expect( + (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), + ).to.equal(1); // Owner mints 1 token with wrong limit and should be reverted. await expect( @@ -1334,12 +1352,14 @@ describe('ERC721M', function () { value: ethers.utils.parseEther('1.0'), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); - + // Owner mints 1 token with valid proof. Now owner reaches the limit. await contract.mintWithLimit(1, 2, ownerProof, 0, '0x00', { value: ethers.utils.parseEther('0.5'), }); - expect((await contract.totalMintedByAddress(owner.getAddress())).toNumber()).to.equal(2); + expect( + (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), + ).to.equal(2); // Owner tries to mint more and reverts. await expect(