diff --git a/contracts/nft/erc721m/ERC721M.sol b/contracts/nft/erc721m/ERC721M.sol index 22d1315b..89902054 100644 --- a/contracts/nft/erc721m/ERC721M.sol +++ b/contracts/nft/erc721m/ERC721M.sol @@ -158,7 +158,7 @@ contract ERC721M is /// @return The stage info, wallet minted count, and stage minted count 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]; diff --git a/test/erc721m/ERC721M.t.sol b/test/erc721m/ERC721M.t.sol new file mode 100644 index 00000000..2c2c1cf0 --- /dev/null +++ b/test/erc721m/ERC721M.t.sol @@ -0,0 +1,595 @@ +// 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; + } +} diff --git a/test/erc721m/ERC721M.test.ts b/test/erc721m/ERC721M.test.ts index 7363d230..2476d188 100644 --- a/test/erc721m/ERC721M.test.ts +++ b/test/erc721m/ERC721M.test.ts @@ -77,700 +77,8 @@ describe('ERC721M', 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; - - 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); - }); - - 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: 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.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); - }); + describe('Minting', function () { + it('mint with waived mint fee', async () => { const [_owner, minter, cosigner] = await ethers.getSigners(); @@ -1566,296 +874,4 @@ describe('ERC721M', 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, - }, - ]); - - // 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'); - }); - }); });