From 5e6727b07d9976882c320d99739f34971fb10d25 Mon Sep 17 00:00:00 2001 From: tenthirtyone Date: Wed, 2 Apr 2025 01:10:35 -0500 Subject: [PATCH 1/6] move some hardhat tests over --- contracts/nft/erc721m/ERC721M.sol | 148 +++++-- test/erc721m/ERC721M.t.sol | 672 ++++++++++++++++++++++++++++++ 2 files changed, 786 insertions(+), 34 deletions(-) create mode 100644 test/erc721m/ERC721M.t.sol diff --git a/contracts/nft/erc721m/ERC721M.sol b/contracts/nft/erc721m/ERC721M.sol index 22d1315b..809634d1 100644 --- a/contracts/nft/erc721m/ERC721M.sol +++ b/contracts/nft/erc721m/ERC721M.sol @@ -75,18 +75,33 @@ contract ERC721M is /// @notice Returns the contract name and version /// @return The contract name and version as strings - function contractNameAndVersion() public pure returns (string memory, string memory) { + function contractNameAndVersion() + public + pure + returns (string memory, string memory) + { return ("ERC721M", "1.0.0"); } /// @notice Gets the token URI for a specific token ID /// @param tokenId The ID of the token /// @return The token URI - function tokenURI(uint256 tokenId) public view override(ERC721A, IERC721A) returns (string memory) { + function tokenURI( + uint256 tokenId + ) public view override(ERC721A, IERC721A) returns (string memory) { if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); string memory baseURI = _currentBaseURI; - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; + return + bytes(baseURI).length != 0 + ? string( + abi.encodePacked( + baseURI, + _toString(tokenId), + _tokenURISuffix + ) + ) + : ""; } /// @notice Gets the contract URI @@ -122,12 +137,13 @@ contract ERC721M is /// @param proof The merkle proof for allowlist minting /// @param timestamp The timestamp for the minting action (used in cosigning) /// @param signature The cosigner's signature - function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) - external - payable - virtual - nonReentrant - { + function mint( + uint32 qty, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) external payable virtual nonReentrant { _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); } @@ -156,9 +172,11 @@ contract ERC721M is /// @notice Gets the stage info for a given stage index /// @param index The stage index /// @return The stage info, wallet minted count, and stage minted count - function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { + function getStageInfo( + uint256 index + ) external view override returns (MintStageInfo memory, uint32, uint256) { if (index >= _mintStages.length) { - revert("InvalidStage"); + revert InvalidStage(); } uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; uint256 stageMinted = _stageMintedCounts[index]; @@ -205,16 +223,23 @@ contract ERC721M is /// @notice Gets the total minted count for a specific address /// @param a The address to get the minted count for /// @return The total minted count - function totalMintedByAddress(address a) external view virtual override returns (uint256) { + function totalMintedByAddress( + address a + ) external view virtual override returns (uint256) { return _numberMinted(a); } /// @notice Gets the active stage from the timestamp /// @param timestamp The timestamp to get the active stage from /// @return The active stage - function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { + function getActiveStageFromTimestamp( + uint256 timestamp + ) public view returns (uint256) { for (uint256 i = 0; i < _mintStages.length; i++) { - if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { + if ( + timestamp >= _mintStages[i].startTimeUnixSeconds && + timestamp < _mintStages[i].endTimeUnixSeconds + ) { return i; } } @@ -233,7 +258,9 @@ contract ERC721M is /// @notice Removes an authorized minter /// @param minter The address to remove as an authorized minter - function removeAuthorizedMinter(address minter) external override onlyOwner { + function removeAuthorizedMinter( + address minter + ) external override onlyOwner { _removeAuthorizedMinter(minter); } @@ -245,7 +272,9 @@ contract ERC721M is /// @notice Sets the timestamp expiry seconds /// @param timestampExpirySeconds The expiry time in seconds for timestamps - function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { + function setTimestampExpirySeconds( + uint256 timestampExpirySeconds + ) external override onlyOwner { _setTimestampExpirySeconds(timestampExpirySeconds); } @@ -257,13 +286,17 @@ contract ERC721M is for (uint256 i = 0; i < newStages.length; i++) { if (i >= 1) { if ( - newStages[i].startTimeUnixSeconds - < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() + newStages[i].startTimeUnixSeconds < + newStages[i - 1].endTimeUnixSeconds + + getTimestampExpirySeconds() ) { revert InsufficientStageTimeGap(); } } - _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); + _assertValidStartAndEndTimestamp( + newStages[i].startTimeUnixSeconds, + newStages[i].endTimeUnixSeconds + ); _mintStages.push( MintStageInfo({ price: newStages[i].price, @@ -295,7 +328,9 @@ contract ERC721M is /// @notice Sets the maximum mintable supply /// @param maxMintableSupply The maximum mintable supply to set - function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { + function setMaxMintableSupply( + uint256 maxMintableSupply + ) external virtual onlyOwner { if (maxMintableSupply > _maxMintableSupply) { revert CannotIncreaseMaxMintableSupply(); } @@ -305,7 +340,9 @@ contract ERC721M is /// @notice Sets the global wallet limit /// @param globalWalletLimit The global wallet limit to set - function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { + function setGlobalWalletLimit( + uint256 globalWalletLimit + ) external onlyOwner { if (globalWalletLimit > _maxMintableSupply) { revert GlobalWalletLimitOverflow(); } @@ -316,19 +353,22 @@ contract ERC721M is /// @notice Allows the owner to mint tokens for a specific address /// @param qty The quantity to mint /// @param to The address to mint tokens for - function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { + function ownerMint( + uint32 qty, + address to + ) external onlyOwner hasSupply(qty) { _safeMint(to, qty); } /// @notice Withdraws the total mint fee and remaining balance from the contract /// @dev Can only be called by the owner function withdraw() external onlyOwner { - (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); + (bool success, ) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); if (!success) revert TransferFailed(); _totalMintFee = 0; uint256 remainingValue = address(this).balance; - (success,) = _fundReceiver.call{value: remainingValue}(""); + (success, ) = _fundReceiver.call{value: remainingValue}(""); if (!success) revert WithdrawFailed(); emit Withdraw(_totalMintFee + remainingValue); @@ -340,14 +380,21 @@ contract ERC721M is if (_mintCurrency == address(0)) revert WrongMintCurrency(); uint256 totalFee = _totalMintFee; - uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); + uint256 remaining = SafeTransferLib.balanceOf( + _mintCurrency, + address(this) + ); if (remaining < totalFee) revert InsufficientBalance(); _totalMintFee = 0; uint256 totalAmount = totalFee + remaining; - SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); + SafeTransferLib.safeTransfer( + _mintCurrency, + MINT_FEE_RECEIVER, + totalFee + ); SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); emit WithdrawERC20(_mintCurrency, totalAmount); @@ -396,7 +443,13 @@ contract ERC721M is bool waiveMintFee = false; if (getCosigner() != address(0)) { - waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); + waiveMintFee = assertValidCosign( + msg.sender, + qty, + timestamp, + signature, + getCosignNonce(msg.sender) + ); _assertValidTimestamp(timestamp); stageTimestamp = timestamp; } @@ -407,7 +460,10 @@ contract ERC721M is uint256 adjustedMintFee = waiveMintFee ? 0 : _mintFee; // Check value if minting with ETH - if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); + if ( + _mintCurrency == address(0) && + msg.value < (stage.price + adjustedMintFee) * qty + ) revert NotEnoughValue(); // Check stage supply if applicable if (stage.maxStageSupply > 0) { @@ -425,19 +481,31 @@ contract ERC721M is // Check wallet limit for stage if applicable, limit == 0 means no limit enforced if (stage.walletLimit > 0) { - if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { + if ( + _stageMintedCountsPerWallet[activeStage][to] + qty > + stage.walletLimit + ) { revert WalletStageLimitExceeded(); } } // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required if (stage.merkleRoot != 0) { - if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { + if ( + !MerkleProofLib.verify( + proof, + stage.merkleRoot, + keccak256(abi.encodePacked(to, limit)) + ) + ) { revert InvalidProof(); } // Verify merkle proof mint limit - if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + if ( + limit > 0 && + _stageMintedCountsPerWallet[activeStage][to] + qty > limit + ) { revert WalletStageLimitExceeded(); } } @@ -445,7 +513,10 @@ contract ERC721M is if (_mintCurrency != address(0)) { // ERC20 mint payment SafeTransferLib.safeTransferFrom( - _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty + _mintCurrency, + msg.sender, + address(this), + (stage.price + adjustedMintFee) * qty ); } @@ -459,12 +530,21 @@ contract ERC721M is /// @notice Validates the start and end timestamps for a stage /// @param start The start timestamp /// @param end The end timestamp - function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { + function _assertValidStartAndEndTimestamp( + uint256 start, + uint256 end + ) internal pure { if (start >= end) revert InvalidStartAndEndTimestamp(); } /// @dev Overriden to prevent double-initialization of the owner. - function _guardInitializeOwner() internal pure virtual override returns (bool) { + function _guardInitializeOwner() + internal + pure + virtual + override + returns (bool) + { return true; } } diff --git a/test/erc721m/ERC721M.t.sol b/test/erc721m/ERC721M.t.sol new file mode 100644 index 00000000..ce1c9d3d --- /dev/null +++ b/test/erc721m/ERC721M.t.sol @@ -0,0 +1,672 @@ +// test/foundry/erc721m/ERC721M.t.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {ERC721M} from "contracts/nft/erc721m/ERC721M.sol"; +import {MintStageInfo} from "contracts/common/Structs.sol"; + +contract ERC721MTest is Test { + ERC721M public erc721m; + + address public owner; + address public fundReceiver; + uint256 public chainId; + + // These live in a library. They should live on the contract at the lowest level possible. + error InsufficientStageTimeGap(); + error InvalidStartAndEndTimestamp(); + error InvalidStage(); + error NotMintable(); + error NotEnoughValue(); + error Reentrancy(); + error CannotIncreaseMaxMintableSupply(); + error NoSupplyLeft(); + error WalletStageLimitExceeded(); + error StageSupplyExceeded(); + error GlobalWalletLimitOverflow(); + error URIQueryForNonexistentToken(); + + function setUp() public { + owner = address(this); + fundReceiver = makeAddr("fundReceiver"); + + chainId = block.chainid; + + erc721m = new ERC721M( + "Test", + "TEST", + "suffix", + 1000, + 1000, + address(this), + 300, + address(0), + fundReceiver, + 0 + ); + } + + function testInitialState() public { + assertEq(erc721m.name(), "Test"); + assertEq(erc721m.symbol(), "TEST"); + assertEq(erc721m.getMaxMintableSupply(), 1000); + assertEq(erc721m.getGlobalWalletLimit(), 1000); + assertEq(erc721m.owner(), owner); + } + + function testContractCanBePausedUnpaused() public { + // starts unpaused + assertTrue(erc721m.getMintable()); + + erc721m.setMintable(false); + assertFalse(erc721m.getMintable()); + + erc721m.setMintable(true); + assertTrue(erc721m.getMintable()); + } + + function testWithdrawByOwner() public { + // Fund contract + deal(address(erc721m), 100); + + uint256 fundReceiverBalanceBefore = address(fundReceiver).balance; + + // Owner withdraws + erc721m.withdraw(); + + // Funds should go to fundReceiver + assertEq(address(erc721m).balance, 0); + assertEq( + address(fundReceiver).balance, + fundReceiverBalanceBefore + 100 + ); + + // Non-owner cannot withdraw + address nonOwner = makeAddr("nonOwner"); + vm.prank(nonOwner); + vm.expectRevert(); + erc721m.withdraw(); + } + + function testDeployment() public { + assertEq(erc721m.getCosigner(), address(this)); + + erc721m.getCosignDigest(owner, 1, false, 0, 0); + } + + function testDeployment0x0Cosigner() public { + address zeroAddress = address(0); + address cosigner = address(1); + + ERC721M erc721mTest = new ERC721M( + "Test", + "TEST", + "test/", + 1000, + 10, + zeroAddress, + 300, + zeroAddress, + fundReceiver, + 0 + ); + + vm.expectRevert(); + erc721mTest.getCosignDigest(owner, 1, false, 0, 0); + + erc721mTest.setCosigner(cosigner); + } + + function testTokenURISuffix() public { + erc721m.setCosigner(address(0)); + erc721m.setTokenURISuffix(".json"); + erc721m.setBaseURI( + "ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/" + ); + // Create stage data + MintStageInfo[] memory stages = new MintStageInfo[](1); + + // Configure the stage fields individually + stages[0].price = uint80(0.1 ether); + stages[0].walletLimit = 0; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 0; + stages[0].startTimeUnixSeconds = block.timestamp; + stages[0].endTimeUnixSeconds = block.timestamp + 1; + + // Set the stages + erc721m.setStages(stages); + + // Create empty proof array for the mint + bytes32[] memory proof = new bytes32[](0); + + // Mint token with required payment + uint256 mintFee = 0; // Adjust if your contract has a mint fee + erc721m.mint{value: 0.11 ether + mintFee}(1, 0, proof, 0, hex"00"); + } + + function testSetStages() public { + // Create stage data with proper Solidity syntax + MintStageInfo[] memory stages = new MintStageInfo[](1); + + // Set values separately to avoid type conversion issues + uint256 price = 0.1 ether; + uint256 startTime = block.timestamp; + uint256 endTime = block.timestamp + 1 days; + + // Configure the stage fields individually + stages[0].price = uint80(price); + stages[0].walletLimit = 5; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 100; + stages[0].startTimeUnixSeconds = startTime; + stages[0].endTimeUnixSeconds = endTime; + + // Set the stages + erc721m.setStages(stages); + + // Verify stage was set correctly + ( + MintStageInfo memory stageInfo, + uint32 walletMinted, + uint256 stageMinted + ) = erc721m.getStageInfo(0); + + assertEq(stageInfo.price, uint80(price)); + assertEq(stageInfo.walletLimit, 5); + assertEq(stageInfo.merkleRoot, bytes32(0)); + assertEq(stageInfo.maxStageSupply, 100); + assertEq(stageInfo.startTimeUnixSeconds, startTime); + assertEq(stageInfo.endTimeUnixSeconds, endTime); + assertEq(walletMinted, 0); + assertEq(stageMinted, 0); + + // Test non-owner cannot set stages + address nonOwner = makeAddr("nonOwner"); + vm.prank(nonOwner); + vm.expectRevert(); + erc721m.setStages(stages); + } + + function testStagesInsufficientGap() public { + MintStageInfo[] memory stages = new MintStageInfo[](2); + + stages[0].price = uint80(0.5 ether); + stages[0].walletLimit = 3; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1; + + stages[1].price = uint80(0.6 ether); + stages[1].walletLimit = 4; + stages[1].merkleRoot = bytes32(0); + stages[1].maxStageSupply = 10; + stages[1].startTimeUnixSeconds = 60; + stages[1].endTimeUnixSeconds = 62; + + vm.expectRevert(InsufficientStageTimeGap.selector); + + erc721m.setStages(stages); + } + + function testStartTime() public { + MintStageInfo[] memory stages = new MintStageInfo[](2); + + stages[0].price = uint80(0.5 ether); + stages[0].walletLimit = 3; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 0; + + stages[1].price = uint80(0.6 ether); + stages[1].walletLimit = 4; + stages[1].merkleRoot = bytes32(0); + stages[1].maxStageSupply = 10; + stages[1].startTimeUnixSeconds = 61; + stages[1].endTimeUnixSeconds = 61; + + vm.expectRevert(InvalidStartAndEndTimestamp.selector); + erc721m.setStages(stages); + + stages[0].startTimeUnixSeconds = 1; + stages[0].endTimeUnixSeconds = 0; + + stages[1].startTimeUnixSeconds = 62; + stages[1].endTimeUnixSeconds = 61; + + vm.expectRevert(InvalidStartAndEndTimestamp.selector); + erc721m.setStages(stages); + } + + function testResetStages() public { + MintStageInfo[] memory stages = new MintStageInfo[](2); + + stages[0].price = uint80(0.5 ether); + stages[0].walletLimit = 3; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1; + + // Some configurable expiry default hidden somewhere in code effects the gap between end/start times. See: getTimestampExpirySeconds + stages[1].price = uint80(0.6 ether); + stages[1].walletLimit = 4; + stages[1].merkleRoot = bytes32(0); + stages[1].maxStageSupply = 10; + stages[1].startTimeUnixSeconds = 301; + stages[1].endTimeUnixSeconds = 302; + + erc721m.setStages(stages); + + assertEq(erc721m.getNumberStages(), 2); + + MintStageInfo[] memory newStages = new MintStageInfo[](1); + + newStages[0].price = uint80(0.7 ether); + newStages[0].walletLimit = 5; + newStages[0].merkleRoot = bytes32(0); + newStages[0].maxStageSupply = 0; + newStages[0].startTimeUnixSeconds = 0; + newStages[0].endTimeUnixSeconds = 1; + + erc721m.setStages(newStages); + + assertEq(erc721m.getNumberStages(), 1); + } + + function testGetStageInfo() public { + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0.5 ether); + stages[0].walletLimit = 3; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1; + + erc721m.setStages(stages); + + ( + MintStageInfo memory stageInfo, + uint32 walletMintedCount, + uint256 stageMinted + ) = erc721m.getStageInfo(0); + + assertEq(stageInfo.price, uint80(0.5 ether)); + assertEq(stageInfo.walletLimit, 3); + assertEq(stageInfo.merkleRoot, bytes32(0)); + assertEq(stageInfo.maxStageSupply, 5); + assertEq(stageInfo.startTimeUnixSeconds, 0); + assertEq(stageInfo.endTimeUnixSeconds, 1); + assertEq(walletMintedCount, 0); + assertEq(stageMinted, 0); + } + + function testRevertGetStageInfoNonExistentStage() public { + vm.expectRevert(InvalidStage.selector); + erc721m.getStageInfo(1); + } + + function testGetActiveStageFromTimestamp() public { + MintStageInfo[] memory stages = new MintStageInfo[](2); + + stages[0].price = uint80(0.5 ether); + stages[0].walletLimit = 3; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1; + + stages[1].price = uint80(0.6 ether); + stages[1].walletLimit = 4; + stages[1].merkleRoot = bytes32(0); + stages[1].maxStageSupply = 10; + stages[1].startTimeUnixSeconds = 301; + stages[1].endTimeUnixSeconds = 302; + + erc721m.setStages(stages); + + assertEq(erc721m.getNumberStages(), 2); + assertEq(erc721m.getActiveStageFromTimestamp(0), 0); + assertEq(erc721m.getActiveStageFromTimestamp(301), 1); + + vm.expectRevert(InvalidStage.selector); + erc721m.getActiveStageFromTimestamp(70); + } + + function testRevertIfNotMintable() public { + erc721m.setMintable(false); + + // Create empty proof array for the mint + bytes32[] memory proof = new bytes32[](0); + + // Mint token with required payment + uint256 mintFee = 0; + + vm.expectRevert(NotMintable.selector); + erc721m.mint{value: 0.11 ether + mintFee}(1, 0, proof, 0, hex"00"); + } + + function testRevertIfWithoutStages() public { + // Create empty proof array for the mint + bytes32[] memory proof = new bytes32[](0); + + // Mint token with required payment + uint256 mintFee = 0; + + erc721m.setCosigner(address(0)); + + vm.expectRevert(InvalidStage.selector); + erc721m.mint{value: 0.11 ether + mintFee}(1, 0, proof, 0, hex"00"); + } + + function testRevertIfNotEnoughValue() public { + erc721m.setCosigner(address(0)); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0.4 ether); + stages[0].walletLimit = 10; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + uint256 mintFee = 0; + + vm.expectRevert(NotEnoughValue.selector); + erc721m.mint{value: 0.399 ether + mintFee}(1, 0, proof, 0, hex"00"); + } + + function testRevertOnReentrancy() public { + TestReentrantExploit exploit = new TestReentrantExploit( + address(erc721m) + ); + + vm.deal(address(exploit), 100 ether); + + erc721m.setCosigner(address(0)); + erc721m.setMintable(true); + erc721m.setCosigner(address(0)); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0.4 ether); + stages[0].walletLimit = 10; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + vm.startPrank(address(exploit)); + vm.expectRevert(Reentrancy.selector); + erc721m.mint{value: 0.4 ether}(1, 0, proof, 0, hex"00"); + vm.stopPrank(); + } + + function testSetMaxMintableSupply() public { + erc721m.setMaxMintableSupply(100); + assertEq(erc721m.getMaxMintableSupply(), 100); + + erc721m.setMaxMintableSupply(100); + assertEq(erc721m.getMaxMintableSupply(), 100); + + erc721m.setMaxMintableSupply(99); + assertEq(erc721m.getMaxMintableSupply(), 99); + + vm.expectRevert(CannotIncreaseMaxMintableSupply.selector); + erc721m.setMaxMintableSupply(101); + } + + function testMintOverMaxMintableSupply() public { + erc721m.setMaxMintableSupply(99); + + erc721m.setCosigner(address(0)); + erc721m.setMintable(true); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0.4 ether); + stages[0].walletLimit = 10; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 5; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + vm.expectRevert(NoSupplyLeft.selector); + erc721m.mint{value: 40 ether}(100, 0, proof, 0, hex"00"); + } + + function testMintWithWalletLimit() public { + erc721m.setMaxMintableSupply(999); + + erc721m.setCosigner(address(0)); + erc721m.setMintable(true); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0.4 ether); + stages[0].walletLimit = 10; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 0; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + erc721m.mint{value: 4 ether}(10, 0, proof, 0, hex"00"); + + vm.expectRevert(WalletStageLimitExceeded.selector); + erc721m.mint{value: 0.4 ether}(1, 0, proof, 0, hex"00"); + } + + function testMintWithLimitedStageSupply() public { + erc721m.setMaxMintableSupply(999); + + erc721m.setCosigner(address(0)); + erc721m.setMintable(true); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0.4 ether); + stages[0].walletLimit = 0; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 10; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + vm.expectRevert(StageSupplyExceeded.selector); + erc721m.mint{value: 4.4 ether}(11, 0, proof, 0, hex"00"); + } + + function testMintForFree() public { + erc721m.setMaxMintableSupply(999); + + erc721m.setCosigner(address(0)); + erc721m.setMintable(true); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0 ether); + stages[0].walletLimit = 0; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 10; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + erc721m.mint{value: 0 ether}(1, 0, proof, 0, hex"00"); + } + + function testMintForFreeWithAFee() public { + ERC721M erc721mFee = new ERC721M( + "Test", + "TEST", + "test/", + 1000, + 1000, + address(this), + 300, + address(0), + fundReceiver, + 0.1 ether + ); + + erc721mFee.setCosigner(address(0)); + erc721mFee.setMintable(true); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0 ether); + stages[0].walletLimit = 0; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 10; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721mFee.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + uint256 initialBalance = address(erc721mFee).balance; + erc721mFee.mint{value: 0.1 ether}(1, 0, proof, 0, hex"00"); + assertEq(address(erc721mFee).balance, initialBalance + 0.1 ether); + } + + function testTokenURI() public { + vm.expectRevert(URIQueryForNonexistentToken.selector); + erc721m.tokenURI(0); + + erc721m.setMaxMintableSupply(999); + + erc721m.setCosigner(address(0)); + erc721m.setMintable(true); + + MintStageInfo[] memory stages = new MintStageInfo[](1); + + stages[0].price = uint80(0 ether); + stages[0].walletLimit = 0; + stages[0].merkleRoot = bytes32(0); + stages[0].maxStageSupply = 10; + stages[0].startTimeUnixSeconds = 0; + stages[0].endTimeUnixSeconds = 1_000_000_000 ether; + + erc721m.setStages(stages); + + bytes32[] memory proof = new bytes32[](0); + + erc721m.mint{value: 0 ether}(1, 0, proof, 0, hex"00"); + + erc721m.setBaseURI("base_uri_"); + assertEq(erc721m.tokenURI(0), "base_uri_0suffix"); + erc721m.setBaseURI(""); + assertEq(erc721m.tokenURI(0), ""); + } + + //describe('Token URI', function () { + + // Helper function to generate signatures + function _getCosignSignature( + address cosigner, + address recipient, + uint256 timestamp, + uint256 qty, + bool feeWaived + ) internal returns (bytes memory) { + bytes32 digest = erc721m.getCosignDigest( + recipient, + uint32(qty), + feeWaived, + 0, + timestamp + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + uint256(keccak256(abi.encodePacked(cosigner))), + digest + ); + return abi.encodePacked(r, s, v); + } + + function testGlobalWalletConstructorLimit() public { + vm.expectRevert(GlobalWalletLimitOverflow.selector); + new ERC721M( + "Test", + "TEST", + "", + 100, + 1001, + address(0), + 60, + address(0), + fundReceiver, + 0.1 ether + ); + } + + function testSetGlobalWalletLimit() public { + erc721m.setGlobalWalletLimit(2); + assertEq(erc721m.getGlobalWalletLimit(), 2); + + vm.expectRevert(GlobalWalletLimitOverflow.selector); + erc721m.setGlobalWalletLimit(1001); + } + + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4) { + return this.onERC721Received.selector; + } +} + +contract TestReentrantExploit { + ERC721M public erc721m; + + constructor(address _erc721m) { + erc721m = ERC721M(_erc721m); + } + + function exploit( + bytes32[] memory proof, + uint256 timestamp, + bytes memory signature + ) public payable { + erc721m.mint{value: 0.4 ether}(1, 0, proof, timestamp, signature); + } + + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4) { + bytes32[] memory proof = new bytes32[](0); + exploit(proof, block.timestamp, hex"00"); + return this.onERC721Received.selector; + } +} From e5623a5b9968ff9d42b1bc8aca97206323a397e9 Mon Sep 17 00:00:00 2001 From: tenthirtyone Date: Wed, 2 Apr 2025 01:12:51 -0500 Subject: [PATCH 2/6] forge fmt --- contracts/nft/erc721m/ERC721M.sol | 146 +++++++----------------------- test/erc721m/ERC721M.t.sol | 129 ++++++-------------------- 2 files changed, 59 insertions(+), 216 deletions(-) diff --git a/contracts/nft/erc721m/ERC721M.sol b/contracts/nft/erc721m/ERC721M.sol index 809634d1..89902054 100644 --- a/contracts/nft/erc721m/ERC721M.sol +++ b/contracts/nft/erc721m/ERC721M.sol @@ -75,33 +75,18 @@ contract ERC721M is /// @notice Returns the contract name and version /// @return The contract name and version as strings - function contractNameAndVersion() - public - pure - returns (string memory, string memory) - { + function contractNameAndVersion() public pure returns (string memory, string memory) { return ("ERC721M", "1.0.0"); } /// @notice Gets the token URI for a specific token ID /// @param tokenId The ID of the token /// @return The token URI - function tokenURI( - uint256 tokenId - ) public view override(ERC721A, IERC721A) returns (string memory) { + function tokenURI(uint256 tokenId) public view override(ERC721A, IERC721A) returns (string memory) { if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); string memory baseURI = _currentBaseURI; - return - bytes(baseURI).length != 0 - ? string( - abi.encodePacked( - baseURI, - _toString(tokenId), - _tokenURISuffix - ) - ) - : ""; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; } /// @notice Gets the contract URI @@ -137,13 +122,12 @@ contract ERC721M is /// @param proof The merkle proof for allowlist minting /// @param timestamp The timestamp for the minting action (used in cosigning) /// @param signature The cosigner's signature - function mint( - uint32 qty, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) external payable virtual nonReentrant { + function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) + external + payable + virtual + nonReentrant + { _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); } @@ -172,9 +156,7 @@ contract ERC721M is /// @notice Gets the stage info for a given stage index /// @param index The stage index /// @return The stage info, wallet minted count, and stage minted count - function getStageInfo( - uint256 index - ) external view override returns (MintStageInfo memory, uint32, uint256) { + function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { if (index >= _mintStages.length) { revert InvalidStage(); } @@ -223,23 +205,16 @@ contract ERC721M is /// @notice Gets the total minted count for a specific address /// @param a The address to get the minted count for /// @return The total minted count - function totalMintedByAddress( - address a - ) external view virtual override returns (uint256) { + function totalMintedByAddress(address a) external view virtual override returns (uint256) { return _numberMinted(a); } /// @notice Gets the active stage from the timestamp /// @param timestamp The timestamp to get the active stage from /// @return The active stage - function getActiveStageFromTimestamp( - uint256 timestamp - ) public view returns (uint256) { + function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { for (uint256 i = 0; i < _mintStages.length; i++) { - if ( - timestamp >= _mintStages[i].startTimeUnixSeconds && - timestamp < _mintStages[i].endTimeUnixSeconds - ) { + if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { return i; } } @@ -258,9 +233,7 @@ contract ERC721M is /// @notice Removes an authorized minter /// @param minter The address to remove as an authorized minter - function removeAuthorizedMinter( - address minter - ) external override onlyOwner { + function removeAuthorizedMinter(address minter) external override onlyOwner { _removeAuthorizedMinter(minter); } @@ -272,9 +245,7 @@ contract ERC721M is /// @notice Sets the timestamp expiry seconds /// @param timestampExpirySeconds The expiry time in seconds for timestamps - function setTimestampExpirySeconds( - uint256 timestampExpirySeconds - ) external override onlyOwner { + function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { _setTimestampExpirySeconds(timestampExpirySeconds); } @@ -286,17 +257,13 @@ contract ERC721M is for (uint256 i = 0; i < newStages.length; i++) { if (i >= 1) { if ( - newStages[i].startTimeUnixSeconds < - newStages[i - 1].endTimeUnixSeconds + - getTimestampExpirySeconds() + newStages[i].startTimeUnixSeconds + < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() ) { revert InsufficientStageTimeGap(); } } - _assertValidStartAndEndTimestamp( - newStages[i].startTimeUnixSeconds, - newStages[i].endTimeUnixSeconds - ); + _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); _mintStages.push( MintStageInfo({ price: newStages[i].price, @@ -328,9 +295,7 @@ contract ERC721M is /// @notice Sets the maximum mintable supply /// @param maxMintableSupply The maximum mintable supply to set - function setMaxMintableSupply( - uint256 maxMintableSupply - ) external virtual onlyOwner { + function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { if (maxMintableSupply > _maxMintableSupply) { revert CannotIncreaseMaxMintableSupply(); } @@ -340,9 +305,7 @@ contract ERC721M is /// @notice Sets the global wallet limit /// @param globalWalletLimit The global wallet limit to set - function setGlobalWalletLimit( - uint256 globalWalletLimit - ) external onlyOwner { + function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { if (globalWalletLimit > _maxMintableSupply) { revert GlobalWalletLimitOverflow(); } @@ -353,22 +316,19 @@ contract ERC721M is /// @notice Allows the owner to mint tokens for a specific address /// @param qty The quantity to mint /// @param to The address to mint tokens for - function ownerMint( - uint32 qty, - address to - ) external onlyOwner hasSupply(qty) { + function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { _safeMint(to, qty); } /// @notice Withdraws the total mint fee and remaining balance from the contract /// @dev Can only be called by the owner function withdraw() external onlyOwner { - (bool success, ) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); + (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); if (!success) revert TransferFailed(); _totalMintFee = 0; uint256 remainingValue = address(this).balance; - (success, ) = _fundReceiver.call{value: remainingValue}(""); + (success,) = _fundReceiver.call{value: remainingValue}(""); if (!success) revert WithdrawFailed(); emit Withdraw(_totalMintFee + remainingValue); @@ -380,21 +340,14 @@ contract ERC721M is if (_mintCurrency == address(0)) revert WrongMintCurrency(); uint256 totalFee = _totalMintFee; - uint256 remaining = SafeTransferLib.balanceOf( - _mintCurrency, - address(this) - ); + uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); if (remaining < totalFee) revert InsufficientBalance(); _totalMintFee = 0; uint256 totalAmount = totalFee + remaining; - SafeTransferLib.safeTransfer( - _mintCurrency, - MINT_FEE_RECEIVER, - totalFee - ); + SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); emit WithdrawERC20(_mintCurrency, totalAmount); @@ -443,13 +396,7 @@ contract ERC721M is bool waiveMintFee = false; if (getCosigner() != address(0)) { - waiveMintFee = assertValidCosign( - msg.sender, - qty, - timestamp, - signature, - getCosignNonce(msg.sender) - ); + waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); _assertValidTimestamp(timestamp); stageTimestamp = timestamp; } @@ -460,10 +407,7 @@ contract ERC721M is uint256 adjustedMintFee = waiveMintFee ? 0 : _mintFee; // Check value if minting with ETH - if ( - _mintCurrency == address(0) && - msg.value < (stage.price + adjustedMintFee) * qty - ) revert NotEnoughValue(); + if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); // Check stage supply if applicable if (stage.maxStageSupply > 0) { @@ -481,31 +425,19 @@ contract ERC721M is // Check wallet limit for stage if applicable, limit == 0 means no limit enforced if (stage.walletLimit > 0) { - if ( - _stageMintedCountsPerWallet[activeStage][to] + qty > - stage.walletLimit - ) { + if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { revert WalletStageLimitExceeded(); } } // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required if (stage.merkleRoot != 0) { - if ( - !MerkleProofLib.verify( - proof, - stage.merkleRoot, - keccak256(abi.encodePacked(to, limit)) - ) - ) { + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { revert InvalidProof(); } // Verify merkle proof mint limit - if ( - limit > 0 && - _stageMintedCountsPerWallet[activeStage][to] + qty > limit - ) { + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { revert WalletStageLimitExceeded(); } } @@ -513,10 +445,7 @@ contract ERC721M is if (_mintCurrency != address(0)) { // ERC20 mint payment SafeTransferLib.safeTransferFrom( - _mintCurrency, - msg.sender, - address(this), - (stage.price + adjustedMintFee) * qty + _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty ); } @@ -530,21 +459,12 @@ contract ERC721M is /// @notice Validates the start and end timestamps for a stage /// @param start The start timestamp /// @param end The end timestamp - function _assertValidStartAndEndTimestamp( - uint256 start, - uint256 end - ) internal pure { + function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { if (start >= end) revert InvalidStartAndEndTimestamp(); } /// @dev Overriden to prevent double-initialization of the owner. - function _guardInitializeOwner() - internal - pure - virtual - override - returns (bool) - { + function _guardInitializeOwner() internal pure virtual override returns (bool) { return true; } } diff --git a/test/erc721m/ERC721M.t.sol b/test/erc721m/ERC721M.t.sol index ce1c9d3d..2c2c1cf0 100644 --- a/test/erc721m/ERC721M.t.sol +++ b/test/erc721m/ERC721M.t.sol @@ -33,18 +33,7 @@ contract ERC721MTest is Test { chainId = block.chainid; - erc721m = new ERC721M( - "Test", - "TEST", - "suffix", - 1000, - 1000, - address(this), - 300, - address(0), - fundReceiver, - 0 - ); + erc721m = new ERC721M("Test", "TEST", "suffix", 1000, 1000, address(this), 300, address(0), fundReceiver, 0); } function testInitialState() public { @@ -77,10 +66,7 @@ contract ERC721MTest is Test { // Funds should go to fundReceiver assertEq(address(erc721m).balance, 0); - assertEq( - address(fundReceiver).balance, - fundReceiverBalanceBefore + 100 - ); + assertEq(address(fundReceiver).balance, fundReceiverBalanceBefore + 100); // Non-owner cannot withdraw address nonOwner = makeAddr("nonOwner"); @@ -99,18 +85,8 @@ contract ERC721MTest is Test { address zeroAddress = address(0); address cosigner = address(1); - ERC721M erc721mTest = new ERC721M( - "Test", - "TEST", - "test/", - 1000, - 10, - zeroAddress, - 300, - zeroAddress, - fundReceiver, - 0 - ); + ERC721M erc721mTest = + new ERC721M("Test", "TEST", "test/", 1000, 10, zeroAddress, 300, zeroAddress, fundReceiver, 0); vm.expectRevert(); erc721mTest.getCosignDigest(owner, 1, false, 0, 0); @@ -121,9 +97,7 @@ contract ERC721MTest is Test { function testTokenURISuffix() public { erc721m.setCosigner(address(0)); erc721m.setTokenURISuffix(".json"); - erc721m.setBaseURI( - "ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/" - ); + erc721m.setBaseURI("ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/"); // Create stage data MintStageInfo[] memory stages = new MintStageInfo[](1); @@ -167,11 +141,7 @@ contract ERC721MTest is Test { erc721m.setStages(stages); // Verify stage was set correctly - ( - MintStageInfo memory stageInfo, - uint32 walletMinted, - uint256 stageMinted - ) = erc721m.getStageInfo(0); + (MintStageInfo memory stageInfo, uint32 walletMinted, uint256 stageMinted) = erc721m.getStageInfo(0); assertEq(stageInfo.price, uint80(price)); assertEq(stageInfo.walletLimit, 5); @@ -289,11 +259,7 @@ contract ERC721MTest is Test { erc721m.setStages(stages); - ( - MintStageInfo memory stageInfo, - uint32 walletMintedCount, - uint256 stageMinted - ) = erc721m.getStageInfo(0); + (MintStageInfo memory stageInfo, uint32 walletMintedCount, uint256 stageMinted) = erc721m.getStageInfo(0); assertEq(stageInfo.price, uint80(0.5 ether)); assertEq(stageInfo.walletLimit, 3); @@ -385,9 +351,7 @@ contract ERC721MTest is Test { } function testRevertOnReentrancy() public { - TestReentrantExploit exploit = new TestReentrantExploit( - address(erc721m) - ); + TestReentrantExploit exploit = new TestReentrantExploit(address(erc721m)); vm.deal(address(exploit), 100 ether); @@ -522,18 +486,8 @@ contract ERC721MTest is Test { } function testMintForFreeWithAFee() public { - ERC721M erc721mFee = new ERC721M( - "Test", - "TEST", - "test/", - 1000, - 1000, - address(this), - 300, - address(0), - fundReceiver, - 0.1 ether - ); + ERC721M erc721mFee = + new ERC721M("Test", "TEST", "test/", 1000, 1000, address(this), 300, address(0), fundReceiver, 0.1 ether); erc721mFee.setCosigner(address(0)); erc721mFee.setMintable(true); @@ -589,41 +543,18 @@ contract ERC721MTest is Test { //describe('Token URI', function () { // Helper function to generate signatures - function _getCosignSignature( - address cosigner, - address recipient, - uint256 timestamp, - uint256 qty, - bool feeWaived - ) internal returns (bytes memory) { - bytes32 digest = erc721m.getCosignDigest( - recipient, - uint32(qty), - feeWaived, - 0, - timestamp - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - uint256(keccak256(abi.encodePacked(cosigner))), - digest - ); + function _getCosignSignature(address cosigner, address recipient, uint256 timestamp, uint256 qty, bool feeWaived) + internal + returns (bytes memory) + { + bytes32 digest = erc721m.getCosignDigest(recipient, uint32(qty), feeWaived, 0, timestamp); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256(keccak256(abi.encodePacked(cosigner))), digest); return abi.encodePacked(r, s, v); } function testGlobalWalletConstructorLimit() public { vm.expectRevert(GlobalWalletLimitOverflow.selector); - new ERC721M( - "Test", - "TEST", - "", - 100, - 1001, - address(0), - 60, - address(0), - fundReceiver, - 0.1 ether - ); + new ERC721M("Test", "TEST", "", 100, 1001, address(0), 60, address(0), fundReceiver, 0.1 ether); } function testSetGlobalWalletLimit() public { @@ -634,12 +565,10 @@ contract ERC721MTest is Test { erc721m.setGlobalWalletLimit(1001); } - function onERC721Received( - address operator, - address from, - uint256 tokenId, - bytes calldata data - ) external returns (bytes4) { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + returns (bytes4) + { return this.onERC721Received.selector; } } @@ -651,20 +580,14 @@ contract TestReentrantExploit { erc721m = ERC721M(_erc721m); } - function exploit( - bytes32[] memory proof, - uint256 timestamp, - bytes memory signature - ) public payable { + function exploit(bytes32[] memory proof, uint256 timestamp, bytes memory signature) public payable { erc721m.mint{value: 0.4 ether}(1, 0, proof, timestamp, signature); } - function onERC721Received( - address operator, - address from, - uint256 tokenId, - bytes calldata data - ) external returns (bytes4) { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + returns (bytes4) + { bytes32[] memory proof = new bytes32[](0); exploit(proof, block.timestamp, hex"00"); return this.onERC721Received.selector; From 7ef18f5c2ce7893cf6fb72d04701416cb88771aa Mon Sep 17 00:00:00 2001 From: tenthirtyone Date: Wed, 2 Apr 2025 01:17:37 -0500 Subject: [PATCH 3/6] remove hardhat tests --- test/erc721m/ERC721CM.test.ts | 1232 +-------------------------------- 1 file changed, 18 insertions(+), 1214 deletions(-) diff --git a/test/erc721m/ERC721CM.test.ts b/test/erc721m/ERC721CM.test.ts index 6cf5ba9b..61179811 100644 --- a/test/erc721m/ERC721CM.test.ts +++ b/test/erc721m/ERC721CM.test.ts @@ -77,670 +77,9 @@ describe('ERC721CM', function () { chainId = await ethers.provider.getNetwork().then((n) => n.chainId); }); - it('Contract can be paused/unpaused', async () => { - // starts unpaused - expect(await contract.getMintable()).to.be.true; - - // we should assert that the correct event is emitted - await expect(contract.setMintable(false)) - .to.emit(contract, 'SetMintable') - .withArgs(false); - expect(await contract.getMintable()).to.be.false; - - // readonlyContract should not be able to setMintable - await expect(readonlyContract.setMintable(true)).to.be.revertedWith( - 'Ownable: caller is not the owner', - ); - }); - - it('withdraws balance by owner', async () => { - // Send 100 wei to contract address for testing. - await ethers.provider.send('hardhat_setBalance', [ - contract.address, - '0x64', // 100 wei - ]); - expect( - (await contract.provider.getBalance(contract.address)).toNumber(), - ).to.equal(100); - - await expect(() => contract.withdraw()).to.changeEtherBalances( - [contract, owner, fundReceiver], - [-100, 0, 100], - ); - - expect( - (await contract.provider.getBalance(contract.address)).toNumber(), - ).to.equal(0); - - // readonlyContract should not be able to withdraw - await expect(readonlyContract.withdraw()).to.be.revertedWith( - 'Ownable: caller is not the owner', - ); - }); describe('Stages', function () { - it('cannot set stages with readonly address', async () => { - await expect( - readonlyContract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]), - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); - - it('cannot set stages with insufficient gap', async () => { - await expect( - contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 60, - endTimeUnixSeconds: 62, - }, - ]), - ).to.be.revertedWith('InsufficientStageTimeGap'); - }); - - it('cannot set stages due to startTimeUnixSeconds is not smaller than endTimeUnixSeconds', async () => { - await expect( - contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 0, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 61, - }, - ]), - ).to.be.revertedWith('InvalidStartAndEndTimestamp'); - - await expect( - contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 1, - endTimeUnixSeconds: 0, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 62, - endTimeUnixSeconds: 61, - }, - ]), - ).to.be.revertedWith('InvalidStartAndEndTimestamp'); - }); - - it('can set / reset stages', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(2); - - let [stageInfo, walletMintedCount] = await contract.getStageInfo(0); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.5')); - expect(stageInfo.walletLimit).to.equal(3); - expect(stageInfo.maxStageSupply).to.equal(5); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x1', 32)); - expect(walletMintedCount).to.equal(0); - - [stageInfo, walletMintedCount] = await contract.getStageInfo(1); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.6')); - expect(stageInfo.walletLimit).to.equal(4); - expect(stageInfo.maxStageSupply).to.equal(10); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x2', 32)); - expect(walletMintedCount).to.equal(0); - - // Update to one stage - await contract.setStages([ - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x3', 32), - maxStageSupply: 0, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(1); - [stageInfo, walletMintedCount] = await contract.getStageInfo(0); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.6')); - expect(stageInfo.walletLimit).to.equal(4); - expect(stageInfo.maxStageSupply).to.equal(0); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x3', 32)); - expect(walletMintedCount).to.equal(0); - - // Add another stage - await contract.setStages([ - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x3', 32), - maxStageSupply: 0, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.7'), - walletLimit: 5, - merkleRoot: ethers.utils.hexZeroPad('0x4', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - expect(await contract.getNumberStages()).to.equal(2); - [stageInfo, walletMintedCount] = await contract.getStageInfo(1); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.7')); - expect(stageInfo.walletLimit).to.equal(5); - expect(stageInfo.maxStageSupply).to.equal(5); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x4', 32)); - expect(walletMintedCount).to.equal(0); - }); - - it('gets stage info', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(1); - - const [stageInfo, walletMintedCount] = await contract.getStageInfo(0); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.5')); - expect(stageInfo.walletLimit).to.equal(3); - expect(stageInfo.maxStageSupply).to.equal(5); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x1', 32)); - expect(walletMintedCount).to.equal(0); - }); - - it('gets stage info reverts for non-existent stage', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - const getStageInfo = readonlyContract.getStageInfo(1); - await expect(getStageInfo).to.be.revertedWith('InvalidStage'); - }); - - it('can find active stage', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(2); - expect(await contract.getActiveStageFromTimestamp(0)).to.equal(0); - - expect(await contract.getActiveStageFromTimestamp(61)).to.equal(1); - - const setActiveStage = contract.getActiveStageFromTimestamp(70); - await expect(setActiveStage).to.be.revertedWith('InvalidStage'); - }); - }); - - describe('Minting', function () { - it('revert if contract is not mintable', async () => { - await contract.setMintable(false); - - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - // not mintable by owner - let mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NotMintable'); - - // not mintable by readonly address - mint = readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NotMintable'); - }); - - it('revert if incorrect (less) amount sent', async () => { - // Get an estimated stage start time - 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: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - await contract.setMintable(true); - - // Setup the test context: block.timestamp should comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - let mint; - mint = contract.mint( - 5, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.499').add(MINT_FEE).mul(5), - }, - ); - await expect(mint).to.be.revertedWith('NotEnoughValue'); - - mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.499999').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NotEnoughValue'); - }); - - it('revert on reentrancy', async () => { - const reentrancyFactory = await ethers.getContractFactory( - 'TestReentrantExploit', - ); - const reentrancyExploiter = await reentrancyFactory.deploy( - contract.address, - ); - await reentrancyExploiter.deployed(); - - // Get an estimated timestamp for the stage start - 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.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x', 32), - maxStageSupply: 0, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 100000, - }, - ]); - await contract.setMintable(true); - - // Setup the test context: block.timestamp should comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await expect( - reentrancyExploiter.exploit(1, [], stageStart, '0x', { - value: ethers.utils.parseEther('0.2').add(MINT_FEE), - }), - ).to.be.revertedWith('Reentrancy'); - }); - - it('can set max mintable supply', async () => { - await contract.setMaxMintableSupply(99); - expect(await contract.getMaxMintableSupply()).to.equal(99); - - // can set the mintable supply again with the same value - await contract.setMaxMintableSupply(99); - expect(await contract.getMaxMintableSupply()).to.equal(99); - - // can set the mintable supply again with the lower value - await contract.setMaxMintableSupply(98); - expect(await contract.getMaxMintableSupply()).to.equal(98); - - // can not set the mintable supply with higher value - await expect(contract.setMaxMintableSupply(100)).to.be.rejectedWith( - 'CannotIncreaseMaxMintableSupply', - ); - - // readonlyContract should not be able to set max mintable supply - await expect( - readonlyContract.setMaxMintableSupply(99), - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); - - it('enforces max mintable supply', async () => { - await contract.setMaxMintableSupply(99); - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - await contract.setMintable(true); - - // Mint 100 tokens (1 over MaxMintableSupply) - const mint = contract.mint( - 100, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('2.5').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NoSupplyLeft'); - }); - - it('mint with unlimited stage limit', async () => { - 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: 100, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 0, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - await contract.setMaxMintableSupply(999); - await contract.setMintable(true); - - // Setup the test context: block.timestamp should comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - // Mint 100 tokens - wallet limit - await contract.mint( - 100, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(100), - }, - ); - - // Mint one more should fail - const mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - - await expect(mint).to.be.revertedWith('WalletStageLimitExceeded'); - }); - - it('mint with unlimited wallet limit', async () => { - 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: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - await contract.setMaxMintableSupply(999); - 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]); - // Mint 100 tokens - stage limit - await contract.mint( - 100, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(100), - }, - ); - - // Mint one more should fail - const mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - - await expect(mint).to.be.revertedWith('StageSupplyExceeded'); - }); - - it('mint with free stage', async () => { - 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: 0, - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - ]); - 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]); - // Mint 100 tokens - stage limit - await readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0').add(MINT_FEE), - }, - ); - const [stageInfo, walletMintedCount, stagedMintedCount] = - await readonlyContract.getStageInfo(0); - expect(stageInfo.maxStageSupply).to.equal(100); - expect(walletMintedCount).to.equal(1); - expect(stagedMintedCount.toNumber()).to.equal(1); - }); - - it('mint with free stage with mint fee', async () => { - 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: 0, - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - ]); - await contract.setMintable(true); - - const contractBalanceInitial = await ethers.provider.getBalance( - contract.address, - ); - const mintFeeReceiverBalanceInitial = - await ethers.provider.getBalance(MINT_FEE_RECEIVER); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }, - ); - - await contract.withdraw(); - - const [stageInfo, walletMintedCount, stagedMintedCount] = - await readonlyContract.getStageInfo(0); - expect(stageInfo.maxStageSupply).to.equal(100); - expect(walletMintedCount).to.equal(1); - expect(stagedMintedCount.toNumber()).to.equal(1); - - const contractBalancePost = await ethers.provider.getBalance( - contract.address, - ); - expect(contractBalancePost.sub(contractBalanceInitial)).to.equal(0); - - const mintFeeReceiverBalancePost = - await ethers.provider.getBalance(MINT_FEE_RECEIVER); - expect( - mintFeeReceiverBalancePost.sub(mintFeeReceiverBalanceInitial), - ).to.equal(MINT_FEE); - }); + it('mint with waived mint fee', async () => { const [_owner, minter, cosigner] = await ethers.getSigners(); @@ -1052,151 +391,28 @@ describe('ERC721CM', function () { minter.address, timestamp, 1, - false, - ); - - // fast forward 2 minutes - await ethers.provider.send('evm_increaseTime', [120]); - await ethers.provider.send('evm_mine', []); - - await expect( - readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - timestamp, - sig, - { - value: ethers.utils.parseEther('0').add(MINT_FEE), - }, - ), - ).to.be.revertedWith('TimestampExpired'); - }); - - it('enforces stage supply', async () => { - 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: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 3, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 10, - startTimeUnixSeconds: stageStart + 63, - endTimeUnixSeconds: stageStart + 66, - }, - ]); - 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]); - // Mint 5 tokens - await expect( - contract.mint(5, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), - }), - ).to.emit(contract, 'Transfer'); - - let [stageInfo, walletMintedCount, stagedMintedCount] = - await contract.getStageInfo(0); - - expect(stageInfo.maxStageSupply).to.equal(5); - expect(walletMintedCount).to.equal(5); - expect(stagedMintedCount.toNumber()).to.equal(5); - - // Mint another 1 should fail since the stage limit has been reached. - let mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('StageSupplyExceeded'); - - // Mint another 5 should fail since the stage limit has been reached. - mint = contract.mint( - 5, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), - }, - ); - await expect(mint).to.be.revertedWith('StageSupplyExceeded'); - - // Setup the test context: Update the block.timestamp to activate the 2nd stage - await ethers.provider.send('evm_mine', [stageStart + 62]); - - await contract.mint( - 8, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(8), - }, - ); - [stageInfo, walletMintedCount, stagedMintedCount] = - await contract.getStageInfo(1); - expect(stageInfo.maxStageSupply).to.equal(10); - expect(walletMintedCount).to.equal(8); - expect(stagedMintedCount.toNumber()).to.equal(8); - - await assert.isRejected( - contract.mint(3, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(3), - }), - /StageSupplyExceeded/, - "Minting more than the stage's supply should fail", - ); - - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(2), - }, + false, ); - [stageInfo, walletMintedCount, stagedMintedCount] = - await contract.getStageInfo(1); - expect(walletMintedCount).to.equal(10); - expect(stagedMintedCount.toNumber()).to.equal(10); - - [stageInfo, walletMintedCount, stagedMintedCount] = - await contract.getStageInfo(0); - expect(walletMintedCount).to.equal(5); - expect(stagedMintedCount.toNumber()).to.equal(5); + // fast forward 2 minutes + await ethers.provider.send('evm_increaseTime', [120]); + await ethers.provider.send('evm_mine', []); - const [address] = await ethers.getSigners(); - const totalMinted = await contract.totalMintedByAddress( - await address.getAddress(), - ); - expect(totalMinted.toNumber()).to.equal(15); + await expect( + readonlyContract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + timestamp, + sig, + { + value: ethers.utils.parseEther('0').add(MINT_FEE), + }, + ), + ).to.be.revertedWith('TimestampExpired'); }); + it('enforces Merkle proof if required', async () => { const accounts = (await ethers.getSigners()).map((signer) => getAddress(signer.address).toLowerCase().trim(), @@ -1283,112 +499,7 @@ 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.1'), - 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.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }); - expect( - (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), - ).to.equal(1); - - // Owner mints 1 token with wrong limit and should be reverted. - await expect( - contract.mint(1, 3, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }), - ).to.be.rejectedWith('InvalidProof'); - - // Owner mints 2 tokens with valid proof and reverts. - await expect( - contract.mint(2, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(2), - }), - ).to.be.rejectedWith('WalletStageLimitExceeded'); - - // Owner mints 1 token with valid proof. Now owner reaches the limit. - await contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }); - expect( - (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), - ).to.equal(2); - - // Owner tries to mint more and reverts. - await expect( - contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }), - ).to.be.rejectedWith('WalletStageLimitExceeded'); - - // Reader mints 6 tokens with valid proof and reverts. - await expect( - readonlyContract.mint(6, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(6), - }), - ).to.be.rejectedWith('WalletStageLimitExceeded'); - - // Reader mints 5 tokens with valid proof. - await readonlyContract.mint(5, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(5), - }); - // Reader mints 1 token with valid proof and reverts. - await expect( - readonlyContract.mint(1, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }), - ).to.be.rejectedWith('WalletStageLimitExceeded'); - }); it('mints by owner', async () => { await contract.setStages([ @@ -1549,312 +660,5 @@ describe('ERC721CM', function () { }); }); - describe('Token URI', function () { - it('Reverts for nonexistent token', async () => { - await expect(contract.tokenURI(0)).to.be.revertedWith( - 'URIQueryForNonexistentToken', - ); - }); - - it('Returns empty tokenURI on empty baseURI', async () => { - 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: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 10, - startTimeUnixSeconds: stageStart + 61, - endTimeUnixSeconds: stageStart + 62, - }, - ]); - 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]); - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('2.5').add(MINT_FEE), - }, - ); - - expect(await contract.tokenURI(0)).to.equal(''); - expect(await contract.tokenURI(1)).to.equal(''); - - await expect(contract.tokenURI(2)).to.be.revertedWith( - 'URIQueryForNonexistentToken', - ); - }); - - it('Returns non-empty tokenURI on non-empty baseURI', async () => { - 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: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 10, - startTimeUnixSeconds: stageStart + 61, - endTimeUnixSeconds: stageStart + 62, - }, - ]); - - await contract.setBaseURI('base_uri_'); - 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]); - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('2.5').add(MINT_FEE), - }, - ); - - expect(await contract.tokenURI(0)).to.equal('base_uri_0'); - expect(await contract.tokenURI(1)).to.equal('base_uri_1'); - - await expect(contract.tokenURI(2)).to.be.revertedWith( - 'URIQueryForNonexistentToken', - ); - }); - }); - - describe('Global wallet limit', function () { - it('validates global wallet limit in constructor', async () => { - const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); - await expect( - ERC721CM.deploy( - 'Test', - 'TEST', - '', - 100, - 1001, - ethers.constants.AddressZero, - 60, - ethers.constants.AddressZero, - fundReceiver.address, - MINT_FEE, - ), - ).to.be.revertedWith('GlobalWalletLimitOverflow'); - }); - - it('sets global wallet limit', async () => { - await contract.setGlobalWalletLimit(2); - expect((await contract.getGlobalWalletLimit()).toNumber()).to.equal(2); - - await expect(contract.setGlobalWalletLimit(1001)).to.be.revertedWith( - 'GlobalWalletLimitOverflow', - ); - }); - - it('enforces global wallet limit', async () => { - await contract.setGlobalWalletLimit(2); - expect((await contract.getGlobalWalletLimit()).toNumber()).to.equal(2); - - 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.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - 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]); - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(2), - }, - ); - - await expect( - contract.mint(1, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }), - ).to.be.revertedWith('WalletGlobalLimitExceeded'); - }); - }); - - describe('Token URI suffix', () => { - it('can set tokenURI suffix', async () => { - await contract.setMintable(true); - await contract.setTokenURISuffix('.json'); - await contract.setBaseURI( - 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/', - ); - - 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.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 0, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - ]); - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - // Mint and verify - await contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }, - ); - - const tokenUri = await contract.tokenURI(0); - expect(tokenUri).to.equal( - 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/0.json', - ); - }); - }); - - describe('Cosign', () => { - it('can deploy with 0x0 cosign', async () => { - const [owner, cosigner, fundReceiver] = await ethers.getSigners(); - const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); - const erc721cm = await ERC721CM.deploy( - 'Test', - 'TEST', - '', - 1000, - 0, - ethers.constants.AddressZero, - 60, - ethers.constants.AddressZero, - fundReceiver.address, - MINT_FEE, - ); - await erc721cm.deployed(); - const ownerConn = erc721cm.connect(owner); - await expect( - ownerConn.getCosignDigest(owner.address, 1, false, 0, 0), - ).to.be.revertedWith('CosignerNotSet'); - - // we can set the cosigner - await ownerConn.setCosigner(cosigner.address); - - // readonly contract can't set cosigner - await expect( - readonlyContract.setCosigner(cosigner.address), - ).to.be.revertedWith('Ownable: caller is not the owner'); - }); - - it('can deploy with cosign', async () => { - const [_, minter, cosigner, fundReceiver] = await ethers.getSigners(); - const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); - const erc721cm = await ERC721CM.deploy( - 'Test', - 'TEST', - '', - 1000, - 0, - cosigner.address, - 60, - ethers.constants.AddressZero, - fundReceiver.address, - MINT_FEE, - ); - await erc721cm.deployed(); - - const minterConn = erc721cm.connect(minter); - const timestamp = Math.floor(new Date().getTime() / 1000); - const sig = await getCosignSignature( - erc721cm, - cosigner, - minter.address, - timestamp, - 1, - false, - ); - await expect( - minterConn.assertValidCosign(minter.address, 1, timestamp, sig, 0), - ).to.not.be.reverted; - - const invalidSig = sig + '00'; - await expect( - minterConn.assertValidCosign( - minter.address, - 1, - timestamp, - invalidSig, - 0, - ), - ).to.be.revertedWith('InvalidCosignSignature'); - }); - }); - describe('Contract URI', function () { - it('can set contract URI', async () => { - await contract.setContractURI( - 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4', - ); - const contractURI = await contract.contractURI(); - expect(contractURI).to.equal( - 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4', - ); - }); - }); }); From 537962d1e663039fc92cfdd7c412e22efa61db40 Mon Sep 17 00:00:00 2001 From: tenthirtyone Date: Wed, 2 Apr 2025 01:23:18 -0500 Subject: [PATCH 4/6] update hh --- test/erc721m/ERC721CM.test.ts | 337 +++++++++++++++++++++++++++++++--- 1 file changed, 315 insertions(+), 22 deletions(-) diff --git a/test/erc721m/ERC721CM.test.ts b/test/erc721m/ERC721CM.test.ts index 61179811..ba67dac8 100644 --- a/test/erc721m/ERC721CM.test.ts +++ b/test/erc721m/ERC721CM.test.ts @@ -8,7 +8,6 @@ import { BigNumber } from 'ethers'; const { getAddress } = ethers.utils; const MINT_FEE_RECEIVER = '0x0B98151bEdeE73f9Ba5F2C7b72dEa02D38Ce49Fc'; -const MINT_FEE = ethers.utils.parseEther('0.00002'); chai.use(chaiAsPromised); @@ -57,7 +56,7 @@ describe('ERC721CM', function () { beforeEach(async () => { [owner, readonly, fundReceiver] = await ethers.getSigners(); - const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); + const ERC721CM = await ethers.getContractFactory('ERC721CM'); const erc721cm = await ERC721CM.deploy( 'Test', 'TEST', @@ -68,7 +67,6 @@ describe('ERC721CM', function () { 60, ethers.constants.AddressZero, fundReceiver.address, - MINT_FEE, ); await erc721cm.deployed(); @@ -77,9 +75,65 @@ describe('ERC721CM', function () { chainId = await ethers.provider.getNetwork().then((n) => n.chainId); }); + describe('Minting', function () { + it('mint with free stage with mint fee', async () => { + 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: 0, + mintFee: ethers.utils.parseEther('0.1'), + walletLimit: 0, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 100, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 1, + }, + ]); + await contract.setMintable(true); + + const contractBalanceInitial = await ethers.provider.getBalance( + contract.address, + ); + const mintFeeReceiverBalanceInitial = + await ethers.provider.getBalance(MINT_FEE_RECEIVER); + + // Setup the test context: Update block.timestamp to comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + await readonlyContract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.1'), + }, + ); - describe('Stages', function () { - + await contract.withdraw(); + + const [stageInfo, walletMintedCount, stagedMintedCount] = + await readonlyContract.getStageInfo(0); + expect(stageInfo.maxStageSupply).to.equal(100); + expect(walletMintedCount).to.equal(1); + expect(stagedMintedCount.toNumber()).to.equal(1); + + const contractBalancePost = await ethers.provider.getBalance( + contract.address, + ); + expect(contractBalancePost.sub(contractBalanceInitial)).to.equal(0); + + const mintFeeReceiverBalancePost = + await ethers.provider.getBalance(MINT_FEE_RECEIVER); + expect( + mintFeeReceiverBalancePost.sub(mintFeeReceiverBalanceInitial), + ).to.equal(ethers.utils.parseEther('0.1')); + }); it('mint with waived mint fee', async () => { const [_owner, minter, cosigner] = await ethers.getSigners(); @@ -91,6 +145,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, + mintFee: ethers.utils.parseEther('0.1'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -158,6 +213,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -184,7 +240,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ); const [stageInfo, walletMintedCount, stagedMintedCount] = @@ -199,6 +255,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -228,7 +285,7 @@ describe('ERC721CM', function () { timestamp + 1, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -242,7 +299,7 @@ describe('ERC721CM', function () { timestamp, sig + '00', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -254,7 +311,7 @@ describe('ERC721CM', function () { timestamp, '0x00', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -266,7 +323,7 @@ describe('ERC721CM', function () { timestamp, '0', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -278,7 +335,7 @@ describe('ERC721CM', function () { timestamp, '', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -292,7 +349,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -336,7 +393,7 @@ describe('ERC721CM', function () { earlyTimestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidStage'); @@ -359,7 +416,7 @@ describe('ERC721CM', function () { lateTimestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidStage'); @@ -374,6 +431,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -406,12 +464,137 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('TimestampExpired'); }); + it('enforces stage supply', async () => { + 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'), + mintFee: 0, + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 5, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 3, + }, + { + price: ethers.utils.parseEther('0.6'), + mintFee: 0, + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 10, + startTimeUnixSeconds: stageStart + 63, + endTimeUnixSeconds: stageStart + 66, + }, + ]); + 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]); + // Mint 5 tokens + await expect( + contract.mint(5, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { + value: ethers.utils.parseEther('2.5'), + }), + ).to.emit(contract, 'Transfer'); + + let [stageInfo, walletMintedCount, stagedMintedCount] = + await contract.getStageInfo(0); + + expect(stageInfo.maxStageSupply).to.equal(5); + expect(walletMintedCount).to.equal(5); + expect(stagedMintedCount.toNumber()).to.equal(5); + + // Mint another 1 should fail since the stage limit has been reached. + let mint = contract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5'), + }, + ); + await expect(mint).to.be.revertedWith('StageSupplyExceeded'); + + // Mint another 5 should fail since the stage limit has been reached. + mint = contract.mint( + 5, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('2.5'), + }, + ); + await expect(mint).to.be.revertedWith('StageSupplyExceeded'); + + // Setup the test context: Update the block.timestamp to activate the 2nd stage + await ethers.provider.send('evm_mine', [stageStart + 62]); + + await contract.mint( + 8, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('4.8'), + }, + ); + [stageInfo, walletMintedCount, stagedMintedCount] = + await contract.getStageInfo(1); + expect(stageInfo.maxStageSupply).to.equal(10); + expect(walletMintedCount).to.equal(8); + expect(stagedMintedCount.toNumber()).to.equal(8); + + await assert.isRejected( + contract.mint(3, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { + value: ethers.utils.parseEther('1.8'), + }), + /StageSupplyExceeded/, + "Minting more than the stage's supply should fail", + ); + + await contract.mint( + 2, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('1.2'), + }, + ); + + [stageInfo, walletMintedCount, stagedMintedCount] = + await contract.getStageInfo(1); + expect(walletMintedCount).to.equal(10); + expect(stagedMintedCount.toNumber()).to.equal(10); + + [stageInfo, walletMintedCount, stagedMintedCount] = + await contract.getStageInfo(0); + expect(walletMintedCount).to.equal(5); + expect(stagedMintedCount.toNumber()).to.equal(5); + + const [address] = await ethers.getSigners(); + const totalMinted = await contract.totalMintedByAddress( + await address.getAddress(), + ); + expect(totalMinted.toNumber()).to.equal(15); + }); it('enforces Merkle proof if required', async () => { const accounts = (await ethers.getSigners()).map((signer) => @@ -456,7 +639,7 @@ describe('ERC721CM', function () { await ethers.provider.send('evm_mine', [stageStart - 1]); // Mint 1 token with valid proof await contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }); const totalMinted = await contract.totalMintedByAddress(signerAddress); expect(totalMinted.toNumber()).to.equal(1); @@ -464,7 +647,7 @@ describe('ERC721CM', function () { // Mint 1 token with someone's else proof should be reverted await expect( readonlyContract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }), ).to.be.rejectedWith('InvalidProof'); }); @@ -481,6 +664,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 5, @@ -494,17 +678,124 @@ describe('ERC721CM', function () { await ethers.provider.send('evm_mine', [stageStart - 1]); // Mint 1 token with invalid proof const mint = contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }); 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.1'), + mintFee: 0, + 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.mint(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.1'), + }); + expect( + (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), + ).to.equal(1); + + // Owner mints 1 token with wrong limit and should be reverted. + await expect( + contract.mint(1, 3, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.1'), + }), + ).to.be.rejectedWith('InvalidProof'); + + // Owner mints 2 tokens with valid proof and reverts. + await expect( + contract.mint(2, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.2'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Owner mints 1 token with valid proof. Now owner reaches the limit. + await contract.mint(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.1'), + }); + expect( + (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), + ).to.equal(2); + // Owner tries to mint more and reverts. + await expect( + contract.mint(1, 2, ownerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.1'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Reader mints 6 tokens with valid proof and reverts. + await expect( + readonlyContract.mint(6, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.6'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + + // Reader mints 5 tokens with valid proof. + await readonlyContract.mint(5, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.5'), + }); + + // Reader mints 1 token with valid proof and reverts. + await expect( + readonlyContract.mint(1, 5, readerProof, 0, '0x00', { + value: ethers.utils.parseEther('0.1'), + }), + ).to.be.rejectedWith('WalletStageLimitExceeded'); + }); it('mints by owner', async () => { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -541,6 +832,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -571,6 +863,7 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 1, @@ -591,7 +884,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }, ); await expect(mint).to.be.revertedWith('NotAuthorized'); @@ -621,7 +914,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1').add(MINT_FEE), + value: ethers.utils.parseEther('1'), }, ), ).to.be.revertedWith('NotAuthorized'); @@ -636,7 +929,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1').add(MINT_FEE), + value: ethers.utils.parseEther('1'), }, ); @@ -653,7 +946,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1').add(MINT_FEE), + value: ethers.utils.parseEther('1'), }, ), ).to.be.revertedWith('NotAuthorized'); From b50951104b823d4582e7b3fdaa436f44df53ca18 Mon Sep 17 00:00:00 2001 From: tenthirtyone Date: Wed, 2 Apr 2025 13:41:23 -0500 Subject: [PATCH 5/6] restore CM. There is no reason why all these duplicate tests should exist --- test/erc721m/ERC721CM.test.ts | 995 ++++++++++++++++++++++++++++-- test/erc721m/ERC721M.test.ts | 1062 +++------------------------------ 2 files changed, 1028 insertions(+), 1029 deletions(-) diff --git a/test/erc721m/ERC721CM.test.ts b/test/erc721m/ERC721CM.test.ts index ba67dac8..6cf5ba9b 100644 --- a/test/erc721m/ERC721CM.test.ts +++ b/test/erc721m/ERC721CM.test.ts @@ -8,6 +8,7 @@ import { BigNumber } from 'ethers'; const { getAddress } = ethers.utils; const MINT_FEE_RECEIVER = '0x0B98151bEdeE73f9Ba5F2C7b72dEa02D38Ce49Fc'; +const MINT_FEE = ethers.utils.parseEther('0.00002'); chai.use(chaiAsPromised); @@ -56,7 +57,7 @@ describe('ERC721CM', function () { beforeEach(async () => { [owner, readonly, fundReceiver] = await ethers.getSigners(); - const ERC721CM = await ethers.getContractFactory('ERC721CM'); + const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); const erc721cm = await ERC721CM.deploy( 'Test', 'TEST', @@ -67,6 +68,7 @@ describe('ERC721CM', function () { 60, ethers.constants.AddressZero, fundReceiver.address, + MINT_FEE, ); await erc721cm.deployed(); @@ -75,7 +77,613 @@ describe('ERC721CM', function () { chainId = await ethers.provider.getNetwork().then((n) => n.chainId); }); + it('Contract can be paused/unpaused', async () => { + // starts unpaused + expect(await contract.getMintable()).to.be.true; + + // we should assert that the correct event is emitted + await expect(contract.setMintable(false)) + .to.emit(contract, 'SetMintable') + .withArgs(false); + expect(await contract.getMintable()).to.be.false; + + // readonlyContract should not be able to setMintable + await expect(readonlyContract.setMintable(true)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + + it('withdraws balance by owner', async () => { + // Send 100 wei to contract address for testing. + await ethers.provider.send('hardhat_setBalance', [ + contract.address, + '0x64', // 100 wei + ]); + expect( + (await contract.provider.getBalance(contract.address)).toNumber(), + ).to.equal(100); + + await expect(() => contract.withdraw()).to.changeEtherBalances( + [contract, owner, fundReceiver], + [-100, 0, 100], + ); + + expect( + (await contract.provider.getBalance(contract.address)).toNumber(), + ).to.equal(0); + + // readonlyContract should not be able to withdraw + await expect(readonlyContract.withdraw()).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + + describe('Stages', function () { + it('cannot set stages with readonly address', async () => { + await expect( + readonlyContract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 61, + endTimeUnixSeconds: 62, + }, + ]), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it('cannot set stages with insufficient gap', async () => { + await expect( + contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 60, + endTimeUnixSeconds: 62, + }, + ]), + ).to.be.revertedWith('InsufficientStageTimeGap'); + }); + + it('cannot set stages due to startTimeUnixSeconds is not smaller than endTimeUnixSeconds', async () => { + await expect( + contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 0, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 61, + endTimeUnixSeconds: 61, + }, + ]), + ).to.be.revertedWith('InvalidStartAndEndTimestamp'); + + await expect( + contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 1, + endTimeUnixSeconds: 0, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 62, + endTimeUnixSeconds: 61, + }, + ]), + ).to.be.revertedWith('InvalidStartAndEndTimestamp'); + }); + + it('can set / reset stages', async () => { + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 61, + endTimeUnixSeconds: 62, + }, + ]); + + expect(await contract.getNumberStages()).to.equal(2); + + let [stageInfo, walletMintedCount] = await contract.getStageInfo(0); + expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.5')); + expect(stageInfo.walletLimit).to.equal(3); + expect(stageInfo.maxStageSupply).to.equal(5); + expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x1', 32)); + expect(walletMintedCount).to.equal(0); + + [stageInfo, walletMintedCount] = await contract.getStageInfo(1); + expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.6')); + expect(stageInfo.walletLimit).to.equal(4); + expect(stageInfo.maxStageSupply).to.equal(10); + expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x2', 32)); + expect(walletMintedCount).to.equal(0); + + // Update to one stage + await contract.setStages([ + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x3', 32), + maxStageSupply: 0, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + ]); + + expect(await contract.getNumberStages()).to.equal(1); + [stageInfo, walletMintedCount] = await contract.getStageInfo(0); + expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.6')); + expect(stageInfo.walletLimit).to.equal(4); + expect(stageInfo.maxStageSupply).to.equal(0); + expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x3', 32)); + expect(walletMintedCount).to.equal(0); + + // Add another stage + await contract.setStages([ + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x3', 32), + maxStageSupply: 0, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + { + price: ethers.utils.parseEther('0.7'), + walletLimit: 5, + merkleRoot: ethers.utils.hexZeroPad('0x4', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 61, + endTimeUnixSeconds: 62, + }, + ]); + expect(await contract.getNumberStages()).to.equal(2); + [stageInfo, walletMintedCount] = await contract.getStageInfo(1); + expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.7')); + expect(stageInfo.walletLimit).to.equal(5); + expect(stageInfo.maxStageSupply).to.equal(5); + expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x4', 32)); + expect(walletMintedCount).to.equal(0); + }); + + it('gets stage info', async () => { + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + ]); + + expect(await contract.getNumberStages()).to.equal(1); + + const [stageInfo, walletMintedCount] = await contract.getStageInfo(0); + expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.5')); + expect(stageInfo.walletLimit).to.equal(3); + expect(stageInfo.maxStageSupply).to.equal(5); + expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x1', 32)); + expect(walletMintedCount).to.equal(0); + }); + + it('gets stage info reverts for non-existent stage', async () => { + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + ]); + + const getStageInfo = readonlyContract.getStageInfo(1); + await expect(getStageInfo).to.be.revertedWith('InvalidStage'); + }); + + it('can find active stage', async () => { + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 3, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 4, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 61, + endTimeUnixSeconds: 62, + }, + ]); + + expect(await contract.getNumberStages()).to.equal(2); + expect(await contract.getActiveStageFromTimestamp(0)).to.equal(0); + + expect(await contract.getActiveStageFromTimestamp(61)).to.equal(1); + + const setActiveStage = contract.getActiveStageFromTimestamp(70); + await expect(setActiveStage).to.be.revertedWith('InvalidStage'); + }); + }); + describe('Minting', function () { + it('revert if contract is not mintable', async () => { + await contract.setMintable(false); + + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + ]); + + // not mintable by owner + let mint = contract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5').add(MINT_FEE), + }, + ); + await expect(mint).to.be.revertedWith('NotMintable'); + + // not mintable by readonly address + mint = readonlyContract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5').add(MINT_FEE), + }, + ); + await expect(mint).to.be.revertedWith('NotMintable'); + }); + + it('revert if incorrect (less) amount sent', async () => { + // Get an estimated stage start time + 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: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 2, + }, + ]); + await contract.setMintable(true); + + // Setup the test context: block.timestamp should comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + let mint; + mint = contract.mint( + 5, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.499').add(MINT_FEE).mul(5), + }, + ); + await expect(mint).to.be.revertedWith('NotEnoughValue'); + + mint = contract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.499999').add(MINT_FEE), + }, + ); + await expect(mint).to.be.revertedWith('NotEnoughValue'); + }); + + it('revert on reentrancy', async () => { + const reentrancyFactory = await ethers.getContractFactory( + 'TestReentrantExploit', + ); + const reentrancyExploiter = await reentrancyFactory.deploy( + contract.address, + ); + await reentrancyExploiter.deployed(); + + // Get an estimated timestamp for the stage start + 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.1'), + walletLimit: 0, + merkleRoot: ethers.utils.hexZeroPad('0x', 32), + maxStageSupply: 0, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 100000, + }, + ]); + await contract.setMintable(true); + + // Setup the test context: block.timestamp should comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + await expect( + reentrancyExploiter.exploit(1, [], stageStart, '0x', { + value: ethers.utils.parseEther('0.2').add(MINT_FEE), + }), + ).to.be.revertedWith('Reentrancy'); + }); + + it('can set max mintable supply', async () => { + await contract.setMaxMintableSupply(99); + expect(await contract.getMaxMintableSupply()).to.equal(99); + + // can set the mintable supply again with the same value + await contract.setMaxMintableSupply(99); + expect(await contract.getMaxMintableSupply()).to.equal(99); + + // can set the mintable supply again with the lower value + await contract.setMaxMintableSupply(98); + expect(await contract.getMaxMintableSupply()).to.equal(98); + + // can not set the mintable supply with higher value + await expect(contract.setMaxMintableSupply(100)).to.be.rejectedWith( + 'CannotIncreaseMaxMintableSupply', + ); + + // readonlyContract should not be able to set max mintable supply + await expect( + readonlyContract.setMaxMintableSupply(99), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it('enforces max mintable supply', async () => { + await contract.setMaxMintableSupply(99); + await contract.setStages([ + { + price: ethers.utils.parseEther('0.5'), + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x1', 32), + maxStageSupply: 5, + startTimeUnixSeconds: 0, + endTimeUnixSeconds: 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x2', 32), + maxStageSupply: 10, + startTimeUnixSeconds: 61, + endTimeUnixSeconds: 62, + }, + ]); + await contract.setMintable(true); + + // Mint 100 tokens (1 over MaxMintableSupply) + const mint = contract.mint( + 100, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('2.5').add(MINT_FEE), + }, + ); + await expect(mint).to.be.revertedWith('NoSupplyLeft'); + }); + + it('mint with unlimited stage limit', async () => { + 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: 100, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 0, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 2, + }, + ]); + await contract.setMaxMintableSupply(999); + await contract.setMintable(true); + + // Setup the test context: block.timestamp should comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + // Mint 100 tokens - wallet limit + await contract.mint( + 100, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(100), + }, + ); + + // Mint one more should fail + const mint = contract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5').add(MINT_FEE), + }, + ); + + await expect(mint).to.be.revertedWith('WalletStageLimitExceeded'); + }); + + it('mint with unlimited wallet limit', async () => { + 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: 0, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 100, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 2, + }, + ]); + await contract.setMaxMintableSupply(999); + 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]); + // Mint 100 tokens - stage limit + await contract.mint( + 100, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(100), + }, + ); + + // Mint one more should fail + const mint = contract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.5').add(MINT_FEE), + }, + ); + + await expect(mint).to.be.revertedWith('StageSupplyExceeded'); + }); + + it('mint with free stage', async () => { + 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: 0, + walletLimit: 0, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 100, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 1, + }, + ]); + 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]); + // Mint 100 tokens - stage limit + await readonlyContract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0').add(MINT_FEE), + }, + ); + const [stageInfo, walletMintedCount, stagedMintedCount] = + await readonlyContract.getStageInfo(0); + expect(stageInfo.maxStageSupply).to.equal(100); + expect(walletMintedCount).to.equal(1); + expect(stagedMintedCount.toNumber()).to.equal(1); + }); + it('mint with free stage with mint fee', async () => { const block = await ethers.provider.getBlock( await ethers.provider.getBlockNumber(), @@ -86,7 +694,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, - mintFee: ethers.utils.parseEther('0.1'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 100, @@ -111,7 +718,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }, ); @@ -132,7 +739,7 @@ describe('ERC721CM', function () { await ethers.provider.getBalance(MINT_FEE_RECEIVER); expect( mintFeeReceiverBalancePost.sub(mintFeeReceiverBalanceInitial), - ).to.equal(ethers.utils.parseEther('0.1')); + ).to.equal(MINT_FEE); }); it('mint with waived mint fee', async () => { @@ -145,7 +752,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, - mintFee: ethers.utils.parseEther('0.1'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -213,7 +819,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, - mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -240,7 +845,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ); const [stageInfo, walletMintedCount, stagedMintedCount] = @@ -255,7 +860,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, - mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -285,7 +889,7 @@ describe('ERC721CM', function () { timestamp + 1, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -299,7 +903,7 @@ describe('ERC721CM', function () { timestamp, sig + '00', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -311,7 +915,7 @@ describe('ERC721CM', function () { timestamp, '0x00', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -323,7 +927,7 @@ describe('ERC721CM', function () { timestamp, '0', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -335,7 +939,7 @@ describe('ERC721CM', function () { timestamp, '', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -349,7 +953,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -393,7 +997,7 @@ describe('ERC721CM', function () { earlyTimestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidStage'); @@ -416,7 +1020,7 @@ describe('ERC721CM', function () { lateTimestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidStage'); @@ -431,7 +1035,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, - mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -464,7 +1067,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('TimestampExpired'); @@ -480,7 +1083,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 10, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 5, @@ -489,7 +1091,6 @@ describe('ERC721CM', function () { }, { price: ethers.utils.parseEther('0.6'), - mintFee: 0, walletLimit: 10, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 10, @@ -504,7 +1105,7 @@ describe('ERC721CM', function () { // Mint 5 tokens await expect( contract.mint(5, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('2.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), }), ).to.emit(contract, 'Transfer'); @@ -523,7 +1124,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }, ); await expect(mint).to.be.revertedWith('StageSupplyExceeded'); @@ -536,7 +1137,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('2.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), }, ); await expect(mint).to.be.revertedWith('StageSupplyExceeded'); @@ -551,7 +1152,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('4.8'), + value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(8), }, ); [stageInfo, walletMintedCount, stagedMintedCount] = @@ -562,7 +1163,7 @@ describe('ERC721CM', function () { await assert.isRejected( contract.mint(3, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('1.8'), + value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(3), }), /StageSupplyExceeded/, "Minting more than the stage's supply should fail", @@ -575,7 +1176,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1.2'), + value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(2), }, ); @@ -639,7 +1240,7 @@ describe('ERC721CM', function () { await ethers.provider.send('evm_mine', [stageStart - 1]); // Mint 1 token with valid proof await contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }); const totalMinted = await contract.totalMintedByAddress(signerAddress); expect(totalMinted.toNumber()).to.equal(1); @@ -647,7 +1248,7 @@ describe('ERC721CM', function () { // Mint 1 token with someone's else proof should be reverted await expect( readonlyContract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }), ).to.be.rejectedWith('InvalidProof'); }); @@ -664,7 +1265,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 5, @@ -678,7 +1278,7 @@ describe('ERC721CM', function () { await ethers.provider.send('evm_mine', [stageStart - 1]); // Mint 1 token with invalid proof const mint = contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }); await expect(mint).to.be.revertedWith('InvalidProof'); }); @@ -722,7 +1322,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.1'), - mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 100, @@ -736,7 +1335,7 @@ describe('ERC721CM', function () { await ethers.provider.send('evm_mine', [stageStart - 1]); // Owner mints 1 token with valid proof await contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }); expect( (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), @@ -745,20 +1344,20 @@ describe('ERC721CM', function () { // Owner mints 1 token with wrong limit and should be reverted. await expect( contract.mint(1, 3, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('InvalidProof'); // Owner mints 2 tokens with valid proof and reverts. await expect( contract.mint(2, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.2'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(2), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Owner mints 1 token with valid proof. Now owner reaches the limit. await contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }); expect( (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), @@ -767,26 +1366,26 @@ describe('ERC721CM', function () { // Owner tries to mint more and reverts. await expect( contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Reader mints 6 tokens with valid proof and reverts. await expect( readonlyContract.mint(6, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.6'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(6), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Reader mints 5 tokens with valid proof. await readonlyContract.mint(5, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(5), }); // Reader mints 1 token with valid proof and reverts. await expect( readonlyContract.mint(1, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); }); @@ -795,7 +1394,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -832,7 +1430,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -863,7 +1460,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 1, @@ -884,7 +1480,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }, ); await expect(mint).to.be.revertedWith('NotAuthorized'); @@ -914,7 +1510,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1'), + value: ethers.utils.parseEther('1').add(MINT_FEE), }, ), ).to.be.revertedWith('NotAuthorized'); @@ -929,7 +1525,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1'), + value: ethers.utils.parseEther('1').add(MINT_FEE), }, ); @@ -946,12 +1542,319 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1'), + value: ethers.utils.parseEther('1').add(MINT_FEE), }, ), ).to.be.revertedWith('NotAuthorized'); }); }); + describe('Token URI', function () { + it('Reverts for nonexistent token', async () => { + await expect(contract.tokenURI(0)).to.be.revertedWith( + 'URIQueryForNonexistentToken', + ); + }); + + it('Returns empty tokenURI on empty baseURI', async () => { + 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: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 5, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 10, + startTimeUnixSeconds: stageStart + 61, + endTimeUnixSeconds: stageStart + 62, + }, + ]); + 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]); + await contract.mint( + 2, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('2.5').add(MINT_FEE), + }, + ); + + expect(await contract.tokenURI(0)).to.equal(''); + expect(await contract.tokenURI(1)).to.equal(''); + + await expect(contract.tokenURI(2)).to.be.revertedWith( + 'URIQueryForNonexistentToken', + ); + }); + + it('Returns non-empty tokenURI on non-empty baseURI', async () => { + 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: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 5, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 1, + }, + { + price: ethers.utils.parseEther('0.6'), + walletLimit: 10, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 10, + startTimeUnixSeconds: stageStart + 61, + endTimeUnixSeconds: stageStart + 62, + }, + ]); + + await contract.setBaseURI('base_uri_'); + 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]); + await contract.mint( + 2, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('2.5').add(MINT_FEE), + }, + ); + + expect(await contract.tokenURI(0)).to.equal('base_uri_0'); + expect(await contract.tokenURI(1)).to.equal('base_uri_1'); + + await expect(contract.tokenURI(2)).to.be.revertedWith( + 'URIQueryForNonexistentToken', + ); + }); + }); + + describe('Global wallet limit', function () { + it('validates global wallet limit in constructor', async () => { + const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); + await expect( + ERC721CM.deploy( + 'Test', + 'TEST', + '', + 100, + 1001, + ethers.constants.AddressZero, + 60, + ethers.constants.AddressZero, + fundReceiver.address, + MINT_FEE, + ), + ).to.be.revertedWith('GlobalWalletLimitOverflow'); + }); + + it('sets global wallet limit', async () => { + await contract.setGlobalWalletLimit(2); + expect((await contract.getGlobalWalletLimit()).toNumber()).to.equal(2); + + await expect(contract.setGlobalWalletLimit(1001)).to.be.revertedWith( + 'GlobalWalletLimitOverflow', + ); + }); + + it('enforces global wallet limit', async () => { + await contract.setGlobalWalletLimit(2); + expect((await contract.getGlobalWalletLimit()).toNumber()).to.equal(2); + + 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.1'), + walletLimit: 0, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 100, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 2, + }, + ]); + 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]); + await contract.mint( + 2, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(2), + }, + ); + + await expect( + contract.mint(1, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { + value: ethers.utils.parseEther('0.1').add(MINT_FEE), + }), + ).to.be.revertedWith('WalletGlobalLimitExceeded'); + }); + }); + + describe('Token URI suffix', () => { + it('can set tokenURI suffix', async () => { + await contract.setMintable(true); + await contract.setTokenURISuffix('.json'); + await contract.setBaseURI( + 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/', + ); + + 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.1'), + walletLimit: 0, + merkleRoot: ethers.utils.hexZeroPad('0x0', 32), + maxStageSupply: 0, + startTimeUnixSeconds: stageStart, + endTimeUnixSeconds: stageStart + 1, + }, + ]); + // Setup the test context: Update block.timestamp to comply to the stage being active + await ethers.provider.send('evm_mine', [stageStart - 1]); + // Mint and verify + await contract.mint( + 1, + 0, + [ethers.utils.hexZeroPad('0x', 32)], + 0, + '0x00', + { + value: ethers.utils.parseEther('0.1').add(MINT_FEE), + }, + ); + + const tokenUri = await contract.tokenURI(0); + expect(tokenUri).to.equal( + 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/0.json', + ); + }); + }); + + describe('Cosign', () => { + it('can deploy with 0x0 cosign', async () => { + const [owner, cosigner, fundReceiver] = await ethers.getSigners(); + const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); + const erc721cm = await ERC721CM.deploy( + 'Test', + 'TEST', + '', + 1000, + 0, + ethers.constants.AddressZero, + 60, + ethers.constants.AddressZero, + fundReceiver.address, + MINT_FEE, + ); + await erc721cm.deployed(); + const ownerConn = erc721cm.connect(owner); + await expect( + ownerConn.getCosignDigest(owner.address, 1, false, 0, 0), + ).to.be.revertedWith('CosignerNotSet'); + + // we can set the cosigner + await ownerConn.setCosigner(cosigner.address); + + // readonly contract can't set cosigner + await expect( + readonlyContract.setCosigner(cosigner.address), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it('can deploy with cosign', async () => { + const [_, minter, cosigner, fundReceiver] = await ethers.getSigners(); + const ERC721CM = await ethers.getContractFactory('contracts/nft/erc721m/ERC721CM.sol:ERC721CM'); + const erc721cm = await ERC721CM.deploy( + 'Test', + 'TEST', + '', + 1000, + 0, + cosigner.address, + 60, + ethers.constants.AddressZero, + fundReceiver.address, + MINT_FEE, + ); + await erc721cm.deployed(); + + const minterConn = erc721cm.connect(minter); + const timestamp = Math.floor(new Date().getTime() / 1000); + const sig = await getCosignSignature( + erc721cm, + cosigner, + minter.address, + timestamp, + 1, + false, + ); + await expect( + minterConn.assertValidCosign(minter.address, 1, timestamp, sig, 0), + ).to.not.be.reverted; + + const invalidSig = sig + '00'; + await expect( + minterConn.assertValidCosign( + minter.address, + 1, + timestamp, + invalidSig, + 0, + ), + ).to.be.revertedWith('InvalidCosignSignature'); + }); + }); + describe('Contract URI', function () { + it('can set contract URI', async () => { + await contract.setContractURI( + 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4', + ); + const contractURI = await contract.contractURI(); + expect(contractURI).to.equal( + 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4', + ); + }); + }); }); diff --git a/test/erc721m/ERC721M.test.ts b/test/erc721m/ERC721M.test.ts index 7363d230..ba67dac8 100644 --- a/test/erc721m/ERC721M.test.ts +++ b/test/erc721m/ERC721M.test.ts @@ -3,25 +3,24 @@ import chai, { assert, expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ethers } from 'hardhat'; import { MerkleTree } from 'merkletreejs'; -import { ERC721M } from '../../typechain-types'; +import { ERC721CM } from '../../typechain-types'; import { BigNumber } from 'ethers'; -const { keccak256, getAddress } = ethers.utils; +const { getAddress } = ethers.utils; const MINT_FEE_RECEIVER = '0x0B98151bEdeE73f9Ba5F2C7b72dEa02D38Ce49Fc'; -const MINT_FEE = ethers.utils.parseEther('0.00002'); chai.use(chaiAsPromised); -describe('ERC721M', function () { - let contract: ERC721M; - let readonlyContract: ERC721M; +describe('ERC721CM', function () { + let contract: ERC721CM; + let readonlyContract: ERC721CM; let owner: SignerWithAddress; let fundReceiver: SignerWithAddress; let readonly: SignerWithAddress; let chainId: number; const getCosignSignature = async ( - contractInstance: ERC721M, + contractInstance: ERC721CM, cosigner: SignerWithAddress, minter: string, timestamp: number, @@ -57,8 +56,8 @@ describe('ERC721M', function () { beforeEach(async () => { [owner, readonly, fundReceiver] = await ethers.getSigners(); - const ERC721M = await ethers.getContractFactory('contracts/nft/erc721m/ERC721M.sol:ERC721M'); - const erc721M = await ERC721M.deploy( + const ERC721CM = await ethers.getContractFactory('ERC721CM'); + const erc721cm = await ERC721CM.deploy( 'Test', 'TEST', '', @@ -68,653 +67,15 @@ describe('ERC721M', function () { 60, ethers.constants.AddressZero, fundReceiver.address, - MINT_FEE, ); - await erc721M.deployed(); + await erc721cm.deployed(); - contract = erc721M.connect(owner); - readonlyContract = erc721M.connect(readonly); + contract = erc721cm.connect(owner); + readonlyContract = erc721cm.connect(readonly); chainId = await ethers.provider.getNetwork().then((n) => n.chainId); }); - it('Contract can be paused/unpaused', async () => { - // starts unpaused - expect(await contract.getMintable()).to.be.true; - - await contract.setMintable(false); - expect(await contract.getMintable()).to.be.false; - - // unpause - await contract.setMintable(true); - expect(await contract.getMintable()).to.be.true; - - // we should assert that the correct event is emitted - await expect(contract.setMintable(false)) - .to.emit(contract, 'SetMintable') - .withArgs(false); - expect(await contract.getMintable()).to.be.false; - - // readonlyContract should not be able to setMintable - await expect(readonlyContract.setMintable(true)).to.be.revertedWith( - 'Unauthorized', - ); - }); - - it('withdraws balance by owner', async () => { - // Send 100 wei to contract address for testing. - await ethers.provider.send('hardhat_setBalance', [ - contract.address, - '0x64', // 100 wei - ]); - expect( - (await contract.provider.getBalance(contract.address)).toNumber(), - ).to.equal(100); - - await expect(() => contract.withdraw()).to.changeEtherBalances( - [contract, owner, fundReceiver], - [-100, 0, 100], - ); - - expect( - (await contract.provider.getBalance(contract.address)).toNumber(), - ).to.equal(0); - - // readonlyContract should not be able to withdraw - await expect(readonlyContract.withdraw()).to.be.revertedWith( - 'Unauthorized', - ); - }); - - describe('Stages', function () { - it('cannot set stages with readonly address', async () => { - await expect( - readonlyContract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]), - ).to.be.revertedWith('Unauthorized'); - }); - - it('cannot set stages with insufficient gap', async () => { - await expect( - contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 60, - endTimeUnixSeconds: 62, - }, - ]), - ).to.be.revertedWith('InsufficientStageTimeGap'); - }); - - it('cannot set stages due to startTimeUnixSeconds is not smaller than endTimeUnixSeconds', async () => { - await expect( - contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 0, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 61, - }, - ]), - ).to.be.revertedWith('InvalidStartAndEndTimestamp'); - - await expect( - contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 1, - endTimeUnixSeconds: 0, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 62, - endTimeUnixSeconds: 61, - }, - ]), - ).to.be.revertedWith('InvalidStartAndEndTimestamp'); - }); - - it('can set / reset stages', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(2); - - let [stageInfo, walletMintedCount] = await contract.getStageInfo(0); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.5')); - expect(stageInfo.walletLimit).to.equal(3); - expect(stageInfo.maxStageSupply).to.equal(5); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x1', 32)); - expect(walletMintedCount).to.equal(0); - - [stageInfo, walletMintedCount] = await contract.getStageInfo(1); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.6')); - expect(stageInfo.walletLimit).to.equal(4); - expect(stageInfo.maxStageSupply).to.equal(10); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x2', 32)); - expect(walletMintedCount).to.equal(0); - - // Update to one stage - await contract.setStages([ - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x3', 32), - maxStageSupply: 0, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(1); - [stageInfo, walletMintedCount] = await contract.getStageInfo(0); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.6')); - expect(stageInfo.walletLimit).to.equal(4); - expect(stageInfo.maxStageSupply).to.equal(0); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x3', 32)); - expect(walletMintedCount).to.equal(0); - - // Add another stage - await contract.setStages([ - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x3', 32), - maxStageSupply: 0, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.7'), - walletLimit: 5, - merkleRoot: ethers.utils.hexZeroPad('0x4', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - expect(await contract.getNumberStages()).to.equal(2); - [stageInfo, walletMintedCount] = await contract.getStageInfo(1); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.7')); - expect(stageInfo.walletLimit).to.equal(5); - expect(stageInfo.maxStageSupply).to.equal(5); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x4', 32)); - expect(walletMintedCount).to.equal(0); - }); - - it('gets stage info', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(1); - - const [stageInfo, walletMintedCount] = await contract.getStageInfo(0); - expect(stageInfo.price).to.equal(ethers.utils.parseEther('0.5')); - expect(stageInfo.walletLimit).to.equal(3); - expect(stageInfo.maxStageSupply).to.equal(5); - expect(stageInfo.merkleRoot).to.equal(ethers.utils.hexZeroPad('0x1', 32)); - expect(walletMintedCount).to.equal(0); - }); - - it('gets stage info reverts for non-existent stage', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - - const getStageInfo = readonlyContract.getStageInfo(1); - await expect(getStageInfo).to.be.revertedWith('InvalidStage'); - }); - - it('can find active stage', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 3, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 4, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - - expect(await contract.getNumberStages()).to.equal(2); - expect(await contract.getActiveStageFromTimestamp(0)).to.equal(0); - - expect(await contract.getActiveStageFromTimestamp(61)).to.equal(1); - - const setActiveStage = contract.getActiveStageFromTimestamp(70); - await expect(setActiveStage).to.be.revertedWith('InvalidStage'); - }); - }); - - describe('Minting', function () { - it('revert if contract is not mintable', async () => { - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - ]); - await contract.setMintable(false); - - // not mintable by owner - let mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NotMintable'); - - // not mintable by readonly address - mint = readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NotMintable'); - }); - - it('revert if contract without stages', async () => { - const mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - - await expect(mint).to.be.revertedWith('InvalidStage'); - }); - - it('revert if incorrect (less) amount sent', async () => { - // Get an estimated stage start time - 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.4'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - - // Setup the test context: block.timestamp should comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - let mint; - mint = contract.mint( - 5, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.399').add(MINT_FEE).mul(5), - }, - ); - await expect(mint).to.be.revertedWith('NotEnoughValue'); - - mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.399999').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NotEnoughValue'); - }); - - it('revert on reentrancy', async () => { - const reentrancyFactory = await ethers.getContractFactory( - 'TestReentrantExploit', - ); - const reentrancyExploiter = await reentrancyFactory.deploy( - contract.address, - ); - await reentrancyExploiter.deployed(); - - // Get an estimated timestamp for the stage start - 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.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x', 32), - maxStageSupply: 0, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 100000, - }, - ]); - - // Setup the test context: block.timestamp should comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await expect( - reentrancyExploiter.exploit(1, [], stageStart, '0x', { - value: ethers.utils.parseEther('0.2').add(MINT_FEE), - }), - ).to.be.revertedWith('Reentrancy'); - }); - - it('can set max mintable supply', async () => { - await contract.setMaxMintableSupply(99); - expect(await contract.getMaxMintableSupply()).to.equal(99); - - // can set the mintable supply again with the same value - await contract.setMaxMintableSupply(99); - expect(await contract.getMaxMintableSupply()).to.equal(99); - - // can set the mintable supply again with the lower value - await contract.setMaxMintableSupply(98); - expect(await contract.getMaxMintableSupply()).to.equal(98); - - // can not set the mintable supply with higher value - await expect(contract.setMaxMintableSupply(100)).to.be.rejectedWith( - 'CannotIncreaseMaxMintableSupply', - ); - - // readonlyContract should not be able to set max mintable supply - await expect( - readonlyContract.setMaxMintableSupply(99), - ).to.be.revertedWith('Unauthorized'); - }); - - it('enforces max mintable supply', async () => { - await contract.setMaxMintableSupply(99); - await contract.setStages([ - { - price: ethers.utils.parseEther('0.5'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x1', 32), - maxStageSupply: 5, - startTimeUnixSeconds: 0, - endTimeUnixSeconds: 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x2', 32), - maxStageSupply: 10, - startTimeUnixSeconds: 61, - endTimeUnixSeconds: 62, - }, - ]); - - // Mint 100 tokens (1 over MaxMintableSupply) - const mint = contract.mint( - 100, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('2.5').add(MINT_FEE), - }, - ); - await expect(mint).to.be.revertedWith('NoSupplyLeft'); - }); - - it('mint with wallet limit', async () => { - 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: 100, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 0, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - await contract.setMaxMintableSupply(999); - - // Setup the test context: block.timestamp should comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - // Mint 100 tokens - wallet limit - await contract.mint( - 100, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(100), - }, - ); - - // Mint one more should fail - const mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - - await expect(mint).to.be.revertedWith('WalletStageLimitExceeded'); - }); - - it('mint with limited stage supply', async () => { - 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.05'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - await contract.setMaxMintableSupply(999); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - // Mint 100 tokens - stage limit - await contract.mint( - 100, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.05').add(MINT_FEE).mul(100), - }, - ); - - // Mint one more should fail - const mint = contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), - }, - ); - - await expect(mint).to.be.revertedWith('StageSupplyExceeded'); - }); - - it('mint with free stage', async () => { - 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'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - ]); - - const contractBalanceInitial = await ethers.provider.getBalance( - contract.address, - ); - const mintFeeReceiverBalanceInitial = - await ethers.provider.getBalance(MINT_FEE_RECEIVER); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0').add(MINT_FEE), - }, - ); - const [stageInfo, walletMintedCount, stagedMintedCount] = - await readonlyContract.getStageInfo(0); - expect(stageInfo.maxStageSupply).to.equal(100); - expect(walletMintedCount).to.equal(1); - expect(stagedMintedCount.toNumber()).to.equal(1); - - const contractBalancePost = await ethers.provider.getBalance( - contract.address, - ); - expect(contractBalancePost.sub(contractBalanceInitial)).to.equal(MINT_FEE); - - const mintFeeReceiverBalancePost = - await ethers.provider.getBalance(MINT_FEE_RECEIVER); - expect( - mintFeeReceiverBalancePost.sub(mintFeeReceiverBalanceInitial), - ).to.equal(0); - }); - + describe('Minting', function () { it('mint with free stage with mint fee', async () => { const block = await ethers.provider.getBlock( await ethers.provider.getBlockNumber(), @@ -724,7 +85,8 @@ describe('ERC721M', function () { // Set stages await contract.setStages([ { - price: ethers.utils.parseEther('0'), + price: 0, + mintFee: ethers.utils.parseEther('0.1'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 100, @@ -732,6 +94,7 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 1, }, ]); + await contract.setMintable(true); const contractBalanceInitial = await ethers.provider.getBalance( contract.address, @@ -748,7 +111,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.1'), }, ); @@ -769,7 +132,7 @@ describe('ERC721M', function () { await ethers.provider.getBalance(MINT_FEE_RECEIVER); expect( mintFeeReceiverBalancePost.sub(mintFeeReceiverBalanceInitial), - ).to.equal(MINT_FEE); + ).to.equal(ethers.utils.parseEther('0.1')); }); it('mint with waived mint fee', async () => { @@ -782,6 +145,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: 0, + mintFee: ethers.utils.parseEther('0.1'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -848,7 +212,8 @@ describe('ERC721M', function () { await contract.setStages([ { - price: ethers.utils.parseEther('0'), + price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -856,9 +221,10 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 1000, }, ]); + await contract.setMintable(true); await contract.setCosigner(cosigner.address); - const timestamp = stageStart + 200; + const timestamp = stageStart + 100; const sig = getCosignSignature( contract, cosigner, @@ -874,7 +240,7 @@ describe('ERC721M', function () { timestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ); const [stageInfo, walletMintedCount, stagedMintedCount] = @@ -888,7 +254,8 @@ describe('ERC721M', function () { const [_owner, minter, cosigner] = await ethers.getSigners(); await contract.setStages([ { - price: ethers.utils.parseEther('0'), + price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -896,6 +263,7 @@ describe('ERC721M', function () { endTimeUnixSeconds: 1, }, ]); + await contract.setMintable(true); await contract.setCosigner(cosigner.address); const timestamp = Math.floor(new Date().getTime() / 1000); @@ -917,7 +285,7 @@ describe('ERC721M', function () { timestamp + 1, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -931,7 +299,7 @@ describe('ERC721M', function () { timestamp, sig + '00', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -943,7 +311,7 @@ describe('ERC721M', function () { timestamp, '0x00', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -955,7 +323,7 @@ describe('ERC721M', function () { timestamp, '0', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -967,7 +335,7 @@ describe('ERC721M', function () { timestamp, '', { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -981,7 +349,7 @@ describe('ERC721M', function () { timestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -995,7 +363,8 @@ describe('ERC721M', function () { const stageStart = block.timestamp; await contract.setStages([ { - price: ethers.utils.parseEther('0'), + price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -1003,6 +372,7 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 1000, }, ]); + await contract.setMintable(true); await contract.setCosigner(cosigner.address); const earlyTimestamp = stageStart - 1; @@ -1023,7 +393,7 @@ describe('ERC721M', function () { earlyTimestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidStage'); @@ -1046,7 +416,7 @@ describe('ERC721M', function () { lateTimestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('InvalidStage'); @@ -1060,7 +430,8 @@ describe('ERC721M', function () { const stageStart = block.timestamp; await contract.setStages([ { - price: ethers.utils.parseEther('0'), + price: 0, + mintFee: 0, walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -1068,6 +439,7 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 1000, }, ]); + await contract.setMintable(true); await contract.setCosigner(cosigner.address); const timestamp = stageStart; @@ -1092,7 +464,7 @@ describe('ERC721M', function () { timestamp, sig, { - value: ethers.utils.parseEther('0').add(MINT_FEE), + value: ethers.utils.parseEther('0'), }, ), ).to.be.revertedWith('TimestampExpired'); @@ -1108,6 +480,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 10, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 5, @@ -1116,6 +489,7 @@ describe('ERC721M', function () { }, { price: ethers.utils.parseEther('0.6'), + mintFee: 0, walletLimit: 10, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 10, @@ -1123,13 +497,14 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 66, }, ]); + 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]); // Mint 5 tokens await expect( contract.mint(5, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), + value: ethers.utils.parseEther('2.5'), }), ).to.emit(contract, 'Transfer'); @@ -1148,7 +523,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }, ); await expect(mint).to.be.revertedWith('StageSupplyExceeded'); @@ -1161,7 +536,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), + value: ethers.utils.parseEther('2.5'), }, ); await expect(mint).to.be.revertedWith('StageSupplyExceeded'); @@ -1176,7 +551,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(8), + value: ethers.utils.parseEther('4.8'), }, ); [stageInfo, walletMintedCount, stagedMintedCount] = @@ -1187,7 +562,7 @@ describe('ERC721M', function () { await assert.isRejected( contract.mint(3, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(3), + value: ethers.utils.parseEther('1.8'), }), /StageSupplyExceeded/, "Minting more than the stage's supply should fail", @@ -1200,7 +575,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(2), + value: ethers.utils.parseEther('1.2'), }, ); @@ -1249,7 +624,8 @@ describe('ERC721M', function () { // Set stages await contract.setStages([ { - price: ethers.utils.parseEther('0.1'), + price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 5, @@ -1257,12 +633,13 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 3, }, ]); + 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]); // Mint 1 token with valid proof await contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }); const totalMinted = await contract.totalMintedByAddress(signerAddress); expect(totalMinted.toNumber()).to.equal(1); @@ -1270,7 +647,7 @@ describe('ERC721M', function () { // Mint 1 token with someone's else proof should be reverted await expect( readonlyContract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }), ).to.be.rejectedWith('InvalidProof'); }); @@ -1287,6 +664,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 5, @@ -1294,12 +672,13 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageStart + 1, }, ]); + 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]); // Mint 1 token with invalid proof const mint = contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }); await expect(mint).to.be.revertedWith('InvalidProof'); }); @@ -1343,6 +722,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.1'), + mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 100, @@ -1350,13 +730,13 @@ describe('ERC721M', function () { 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.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.1'), }); expect( (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), @@ -1365,20 +745,20 @@ describe('ERC721M', function () { // Owner mints 1 token with wrong limit and should be reverted. await expect( contract.mint(1, 3, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.1'), }), ).to.be.rejectedWith('InvalidProof'); // Owner mints 2 tokens with valid proof and reverts. await expect( contract.mint(2, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(2), + value: ethers.utils.parseEther('0.2'), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Owner mints 1 token with valid proof. Now owner reaches the limit. await contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.1'), }); expect( (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), @@ -1387,26 +767,26 @@ describe('ERC721M', function () { // Owner tries to mint more and reverts. await expect( contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.1'), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Reader mints 6 tokens with valid proof and reverts. await expect( readonlyContract.mint(6, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(6), + value: ethers.utils.parseEther('0.6'), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Reader mints 5 tokens with valid proof. await readonlyContract.mint(5, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(5), + value: ethers.utils.parseEther('0.5'), }); // Reader mints 1 token with valid proof and reverts. await expect( readonlyContract.mint(1, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1').add(MINT_FEE), + value: ethers.utils.parseEther('0.1'), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); }); @@ -1415,6 +795,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -1422,6 +803,7 @@ describe('ERC721M', function () { endTimeUnixSeconds: 1, }, ]); + await contract.setMintable(true); const [owner, address1] = await ethers.getSigners(); @@ -1450,6 +832,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -1457,6 +840,7 @@ describe('ERC721M', function () { endTimeUnixSeconds: 1, }, ]); + await contract.setMintable(true); await expect( contract.ownerMint(1001, readonly.address), ).to.be.revertedWith('NoSupplyLeft'); @@ -1479,6 +863,7 @@ describe('ERC721M', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), + mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 1, @@ -1486,6 +871,8 @@ describe('ERC721M', function () { endTimeUnixSeconds: stageEnd, }, ]); + + await contract.setMintable(true); }); it('revert if not authorized minter', async () => { @@ -1497,7 +884,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5').add(MINT_FEE), + value: ethers.utils.parseEther('0.5'), }, ); await expect(mint).to.be.revertedWith('NotAuthorized'); @@ -1527,7 +914,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('1').add(MINT_FEE), + value: ethers.utils.parseEther('1'), }, ), ).to.be.revertedWith('NotAuthorized'); @@ -1542,7 +929,7 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('1').add(MINT_FEE), + value: ethers.utils.parseEther('1'), }, ); @@ -1559,303 +946,12 @@ describe('ERC721M', function () { 0, '0x00', { - value: ethers.utils.parseEther('1').add(MINT_FEE), + value: ethers.utils.parseEther('1'), }, ), ).to.be.revertedWith('NotAuthorized'); }); }); - describe('Token URI', function () { - it('Reverts for nonexistent token', async () => { - await expect(contract.tokenURI(0)).to.be.revertedWith( - 'URIQueryForNonexistentToken', - ); - }); - - it('Returns empty tokenURI on empty baseURI', async () => { - 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: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 10, - startTimeUnixSeconds: stageStart + 61, - endTimeUnixSeconds: stageStart + 62, - }, - ]); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('2.5').add(MINT_FEE), - }, - ); - - expect(await contract.tokenURI(0)).to.equal(''); - expect(await contract.tokenURI(1)).to.equal(''); - - await expect(contract.tokenURI(2)).to.be.revertedWith( - 'URIQueryForNonexistentToken', - ); - }); - - it('Returns non-empty tokenURI on non-empty baseURI', async () => { - 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: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 5, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - { - price: ethers.utils.parseEther('0.6'), - walletLimit: 10, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 10, - startTimeUnixSeconds: stageStart + 61, - endTimeUnixSeconds: stageStart + 62, - }, - ]); - - await contract.setBaseURI('base_uri_'); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('2.5').add(MINT_FEE), - }, - ); - - expect(await contract.tokenURI(0)).to.equal('base_uri_0'); - expect(await contract.tokenURI(1)).to.equal('base_uri_1'); - - await expect(contract.tokenURI(2)).to.be.revertedWith( - 'URIQueryForNonexistentToken', - ); - }); - }); - - describe('Global wallet limit', function () { - it('validates global wallet limit in constructor', async () => { - const ERC721M = await ethers.getContractFactory('contracts/nft/erc721m/ERC721M.sol:ERC721M'); - await expect( - ERC721M.deploy( - 'Test', - 'TEST', - '', - 100, - 1001, - ethers.constants.AddressZero, - 60, - ethers.constants.AddressZero, - fundReceiver.address, - MINT_FEE, - ), - ).to.be.revertedWith('GlobalWalletLimitOverflow'); - }); - - it('sets global wallet limit', async () => { - await contract.setGlobalWalletLimit(2); - expect((await contract.getGlobalWalletLimit()).toNumber()).to.equal(2); - - await expect(contract.setGlobalWalletLimit(1001)).to.be.revertedWith( - 'GlobalWalletLimitOverflow', - ); - }); - - it('enforces global wallet limit', async () => { - await contract.setGlobalWalletLimit(2); - expect((await contract.getGlobalWalletLimit()).toNumber()).to.equal(2); - - 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.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 2, - }, - ]); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await contract.mint( - 2, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.11').add(MINT_FEE).mul(2), - }, - ); - - await expect( - contract.mint(1, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('0.11'), - }), - ).to.be.revertedWith('WalletGlobalLimitExceeded'); - }); - }); - - describe('Token URI suffix', () => { - it('can set tokenURI suffix', async () => { - await contract.setTokenURISuffix('.json'); - await contract.setBaseURI( - 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/', - ); - - 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.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 0, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - ]); - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - // Mint and verify - await contract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.11').add(MINT_FEE), - }, - ); - - const tokenUri = await contract.tokenURI(0); - expect(tokenUri).to.equal( - 'ipfs://bafybeidntqfipbuvdhdjosntmpxvxyse2dkyfpa635u4g6txruvt5qf7y4/0.json', - ); - }); - }); - - describe('Cosign', () => { - it('can deploy with 0x0 cosign', async () => { - const [owner, cosigner, fundReceiver] = await ethers.getSigners(); - const ERC721M = await ethers.getContractFactory('contracts/nft/erc721m/ERC721M.sol:ERC721M'); - const erc721M = await ERC721M.deploy( - 'Test', - 'TEST', - '', - 1000, - 0, - ethers.constants.AddressZero, - 60, - ethers.constants.AddressZero, - fundReceiver.address, - MINT_FEE, - ); - await erc721M.deployed(); - const ownerConn = erc721M.connect(owner); - await expect( - ownerConn.getCosignDigest(owner.address, 1, false, 0, 0), - ).to.be.revertedWith('CosignerNotSet'); - - // we can set the cosigner - await ownerConn.setCosigner(cosigner.address); - - // readonly contract can't set cosigner - await expect( - readonlyContract.setCosigner(cosigner.address), - ).to.be.revertedWith('Unauthorized'); - }); - - it('can deploy with cosign', async () => { - const [_, minter, cosigner, fundReceiver] = await ethers.getSigners(); - const ERC721M = await ethers.getContractFactory('contracts/nft/erc721m/ERC721M.sol:ERC721M'); - const erc721M = await ERC721M.deploy( - 'Test', - 'TEST', - '', - 1000, - 0, - cosigner.address, - 60, - ethers.constants.AddressZero, - fundReceiver.address, - MINT_FEE, - ); - await erc721M.deployed(); - - const minterConn = erc721M.connect(minter); - const timestamp = Math.floor(new Date().getTime() / 1000); - const sig = await getCosignSignature( - erc721M, - cosigner, - minter.address, - timestamp, - 1, - false, - ); - await expect( - minterConn.assertValidCosign(minter.address, 1, timestamp, sig, 0), - ).to.not.be.reverted; - const invalidSig = sig + '00'; - await expect( - minterConn.assertValidCosign( - minter.address, - 1, - timestamp, - invalidSig, - 0, - ), - ).to.be.revertedWith('InvalidCosignSignature'); - }); - }); }); From 7083c07e821217d7f6897116c9f9d7a3cd2d666b Mon Sep 17 00:00:00 2001 From: tenthirtyone Date: Wed, 2 Apr 2025 13:42:31 -0500 Subject: [PATCH 6/6] remove C tests that are duped by foundry now --- test/erc721m/ERC721M.test.ts | 184 ++++++++++------------------------- 1 file changed, 52 insertions(+), 132 deletions(-) diff --git a/test/erc721m/ERC721M.test.ts b/test/erc721m/ERC721M.test.ts index ba67dac8..2476d188 100644 --- a/test/erc721m/ERC721M.test.ts +++ b/test/erc721m/ERC721M.test.ts @@ -3,24 +3,25 @@ import chai, { assert, expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ethers } from 'hardhat'; import { MerkleTree } from 'merkletreejs'; -import { ERC721CM } from '../../typechain-types'; +import { ERC721M } from '../../typechain-types'; import { BigNumber } from 'ethers'; -const { getAddress } = ethers.utils; +const { keccak256, getAddress } = ethers.utils; const MINT_FEE_RECEIVER = '0x0B98151bEdeE73f9Ba5F2C7b72dEa02D38Ce49Fc'; +const MINT_FEE = ethers.utils.parseEther('0.00002'); chai.use(chaiAsPromised); -describe('ERC721CM', function () { - let contract: ERC721CM; - let readonlyContract: ERC721CM; +describe('ERC721M', function () { + let contract: ERC721M; + let readonlyContract: ERC721M; let owner: SignerWithAddress; let fundReceiver: SignerWithAddress; let readonly: SignerWithAddress; let chainId: number; const getCosignSignature = async ( - contractInstance: ERC721CM, + contractInstance: ERC721M, cosigner: SignerWithAddress, minter: string, timestamp: number, @@ -56,8 +57,8 @@ describe('ERC721CM', function () { beforeEach(async () => { [owner, readonly, fundReceiver] = await ethers.getSigners(); - const ERC721CM = await ethers.getContractFactory('ERC721CM'); - const erc721cm = await ERC721CM.deploy( + const ERC721M = await ethers.getContractFactory('contracts/nft/erc721m/ERC721M.sol:ERC721M'); + const erc721M = await ERC721M.deploy( 'Test', 'TEST', '', @@ -67,73 +68,17 @@ describe('ERC721CM', function () { 60, ethers.constants.AddressZero, fundReceiver.address, + MINT_FEE, ); - await erc721cm.deployed(); + await erc721M.deployed(); - contract = erc721cm.connect(owner); - readonlyContract = erc721cm.connect(readonly); + contract = erc721M.connect(owner); + readonlyContract = erc721M.connect(readonly); chainId = await ethers.provider.getNetwork().then((n) => n.chainId); }); describe('Minting', function () { - it('mint with free stage with mint fee', async () => { - 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: 0, - mintFee: ethers.utils.parseEther('0.1'), - walletLimit: 0, - merkleRoot: ethers.utils.hexZeroPad('0x0', 32), - maxStageSupply: 100, - startTimeUnixSeconds: stageStart, - endTimeUnixSeconds: stageStart + 1, - }, - ]); - await contract.setMintable(true); - - const contractBalanceInitial = await ethers.provider.getBalance( - contract.address, - ); - const mintFeeReceiverBalanceInitial = - await ethers.provider.getBalance(MINT_FEE_RECEIVER); - - // Setup the test context: Update block.timestamp to comply to the stage being active - await ethers.provider.send('evm_mine', [stageStart - 1]); - await readonlyContract.mint( - 1, - 0, - [ethers.utils.hexZeroPad('0x', 32)], - 0, - '0x00', - { - value: ethers.utils.parseEther('0.1'), - }, - ); - - await contract.withdraw(); - - const [stageInfo, walletMintedCount, stagedMintedCount] = - await readonlyContract.getStageInfo(0); - expect(stageInfo.maxStageSupply).to.equal(100); - expect(walletMintedCount).to.equal(1); - expect(stagedMintedCount.toNumber()).to.equal(1); - - const contractBalancePost = await ethers.provider.getBalance( - contract.address, - ); - expect(contractBalancePost.sub(contractBalanceInitial)).to.equal(0); - - const mintFeeReceiverBalancePost = - await ethers.provider.getBalance(MINT_FEE_RECEIVER); - expect( - mintFeeReceiverBalancePost.sub(mintFeeReceiverBalanceInitial), - ).to.equal(ethers.utils.parseEther('0.1')); - }); + it('mint with waived mint fee', async () => { const [_owner, minter, cosigner] = await ethers.getSigners(); @@ -145,7 +90,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: 0, - mintFee: ethers.utils.parseEther('0.1'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -212,8 +156,7 @@ describe('ERC721CM', function () { await contract.setStages([ { - price: 0, - mintFee: 0, + price: ethers.utils.parseEther('0'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x', 32), maxStageSupply: 100, @@ -221,10 +164,9 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageStart + 1000, }, ]); - await contract.setMintable(true); await contract.setCosigner(cosigner.address); - const timestamp = stageStart + 100; + const timestamp = stageStart + 200; const sig = getCosignSignature( contract, cosigner, @@ -240,7 +182,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ); const [stageInfo, walletMintedCount, stagedMintedCount] = @@ -254,8 +196,7 @@ describe('ERC721CM', function () { const [_owner, minter, cosigner] = await ethers.getSigners(); await contract.setStages([ { - price: 0, - mintFee: 0, + price: ethers.utils.parseEther('0'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -263,7 +204,6 @@ describe('ERC721CM', function () { endTimeUnixSeconds: 1, }, ]); - await contract.setMintable(true); await contract.setCosigner(cosigner.address); const timestamp = Math.floor(new Date().getTime() / 1000); @@ -285,7 +225,7 @@ describe('ERC721CM', function () { timestamp + 1, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -299,7 +239,7 @@ describe('ERC721CM', function () { timestamp, sig + '00', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -311,7 +251,7 @@ describe('ERC721CM', function () { timestamp, '0x00', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -323,7 +263,7 @@ describe('ERC721CM', function () { timestamp, '0', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -335,7 +275,7 @@ describe('ERC721CM', function () { timestamp, '', { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.rejectedWith('invalid arrayify'); @@ -349,7 +289,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidCosignSignature'); @@ -363,8 +303,7 @@ describe('ERC721CM', function () { const stageStart = block.timestamp; await contract.setStages([ { - price: 0, - mintFee: 0, + price: ethers.utils.parseEther('0'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -372,7 +311,6 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageStart + 1000, }, ]); - await contract.setMintable(true); await contract.setCosigner(cosigner.address); const earlyTimestamp = stageStart - 1; @@ -393,7 +331,7 @@ describe('ERC721CM', function () { earlyTimestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidStage'); @@ -416,7 +354,7 @@ describe('ERC721CM', function () { lateTimestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('InvalidStage'); @@ -430,8 +368,7 @@ describe('ERC721CM', function () { const stageStart = block.timestamp; await contract.setStages([ { - price: 0, - mintFee: 0, + price: ethers.utils.parseEther('0'), walletLimit: 0, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 100, @@ -439,7 +376,6 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageStart + 1000, }, ]); - await contract.setMintable(true); await contract.setCosigner(cosigner.address); const timestamp = stageStart; @@ -464,7 +400,7 @@ describe('ERC721CM', function () { timestamp, sig, { - value: ethers.utils.parseEther('0'), + value: ethers.utils.parseEther('0').add(MINT_FEE), }, ), ).to.be.revertedWith('TimestampExpired'); @@ -480,7 +416,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 10, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 5, @@ -489,7 +424,6 @@ describe('ERC721CM', function () { }, { price: ethers.utils.parseEther('0.6'), - mintFee: 0, walletLimit: 10, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 10, @@ -497,14 +431,13 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageStart + 66, }, ]); - 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]); // Mint 5 tokens await expect( contract.mint(5, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('2.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), }), ).to.emit(contract, 'Transfer'); @@ -523,7 +456,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }, ); await expect(mint).to.be.revertedWith('StageSupplyExceeded'); @@ -536,7 +469,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('2.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE).mul(5), }, ); await expect(mint).to.be.revertedWith('StageSupplyExceeded'); @@ -551,7 +484,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('4.8'), + value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(8), }, ); [stageInfo, walletMintedCount, stagedMintedCount] = @@ -562,7 +495,7 @@ describe('ERC721CM', function () { await assert.isRejected( contract.mint(3, 0, [ethers.utils.hexZeroPad('0x', 32)], 0, '0x00', { - value: ethers.utils.parseEther('1.8'), + value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(3), }), /StageSupplyExceeded/, "Minting more than the stage's supply should fail", @@ -575,7 +508,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1.2'), + value: ethers.utils.parseEther('0.6').add(MINT_FEE).mul(2), }, ); @@ -624,8 +557,7 @@ describe('ERC721CM', function () { // Set stages await contract.setStages([ { - price: ethers.utils.parseEther('0.5'), - mintFee: 0, + price: ethers.utils.parseEther('0.1'), walletLimit: 10, merkleRoot: root, maxStageSupply: 5, @@ -633,13 +565,12 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageStart + 3, }, ]); - 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]); // Mint 1 token with valid proof await contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }); const totalMinted = await contract.totalMintedByAddress(signerAddress); expect(totalMinted.toNumber()).to.equal(1); @@ -647,7 +578,7 @@ describe('ERC721CM', function () { // Mint 1 token with someone's else proof should be reverted await expect( readonlyContract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('InvalidProof'); }); @@ -664,7 +595,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 5, @@ -672,13 +602,12 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageStart + 1, }, ]); - 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]); // Mint 1 token with invalid proof const mint = contract.mint(1, 0, proof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }); await expect(mint).to.be.revertedWith('InvalidProof'); }); @@ -722,7 +651,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.1'), - mintFee: 0, walletLimit: 10, merkleRoot: root, maxStageSupply: 100, @@ -730,13 +658,13 @@ describe('ERC721CM', function () { 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.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }); expect( (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), @@ -745,20 +673,20 @@ describe('ERC721CM', function () { // Owner mints 1 token with wrong limit and should be reverted. await expect( contract.mint(1, 3, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('InvalidProof'); // Owner mints 2 tokens with valid proof and reverts. await expect( contract.mint(2, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.2'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(2), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Owner mints 1 token with valid proof. Now owner reaches the limit. await contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }); expect( (await contract.totalMintedByAddress(owner.getAddress())).toNumber(), @@ -767,26 +695,26 @@ describe('ERC721CM', function () { // Owner tries to mint more and reverts. await expect( contract.mint(1, 2, ownerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Reader mints 6 tokens with valid proof and reverts. await expect( readonlyContract.mint(6, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.6'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(6), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); // Reader mints 5 tokens with valid proof. await readonlyContract.mint(5, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE).mul(5), }); // Reader mints 1 token with valid proof and reverts. await expect( readonlyContract.mint(1, 5, readerProof, 0, '0x00', { - value: ethers.utils.parseEther('0.1'), + value: ethers.utils.parseEther('0.1').add(MINT_FEE), }), ).to.be.rejectedWith('WalletStageLimitExceeded'); }); @@ -795,7 +723,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -803,7 +730,6 @@ describe('ERC721CM', function () { endTimeUnixSeconds: 1, }, ]); - await contract.setMintable(true); const [owner, address1] = await ethers.getSigners(); @@ -832,7 +758,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x1', 32), maxStageSupply: 1, @@ -840,7 +765,6 @@ describe('ERC721CM', function () { endTimeUnixSeconds: 1, }, ]); - await contract.setMintable(true); await expect( contract.ownerMint(1001, readonly.address), ).to.be.revertedWith('NoSupplyLeft'); @@ -863,7 +787,6 @@ describe('ERC721CM', function () { await contract.setStages([ { price: ethers.utils.parseEther('0.5'), - mintFee: 0, walletLimit: 1, merkleRoot: ethers.utils.hexZeroPad('0x0', 32), maxStageSupply: 1, @@ -871,8 +794,6 @@ describe('ERC721CM', function () { endTimeUnixSeconds: stageEnd, }, ]); - - await contract.setMintable(true); }); it('revert if not authorized minter', async () => { @@ -884,7 +805,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('0.5'), + value: ethers.utils.parseEther('0.5').add(MINT_FEE), }, ); await expect(mint).to.be.revertedWith('NotAuthorized'); @@ -914,7 +835,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1'), + value: ethers.utils.parseEther('1').add(MINT_FEE), }, ), ).to.be.revertedWith('NotAuthorized'); @@ -929,7 +850,7 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1'), + value: ethers.utils.parseEther('1').add(MINT_FEE), }, ); @@ -946,12 +867,11 @@ describe('ERC721CM', function () { 0, '0x00', { - value: ethers.utils.parseEther('1'), + value: ethers.utils.parseEther('1').add(MINT_FEE), }, ), ).to.be.revertedWith('NotAuthorized'); }); }); - });