diff --git a/.storage-layout b/.storage-layout index 60d4f71..3979d59 100644 --- a/.storage-layout +++ b/.storage-layout @@ -19,17 +19,20 @@ ➡ Auction ======================= -| Name | Type | Slot | Offset | Bytes | Contract | -|---------------|--------------------------------|------|--------|-------|---------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/auction/Auction.sol:Auction | -| _initializing | bool | 0 | 1 | 1 | src/auction/Auction.sol:Auction | -| _owner | address | 0 | 2 | 20 | src/auction/Auction.sol:Auction | -| _pendingOwner | address | 1 | 0 | 20 | src/auction/Auction.sol:Auction | -| _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | -| _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | -| settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | -| token | contract IBaseToken | 6 | 0 | 20 | src/auction/Auction.sol:Auction | -| auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | +| Name | Type | Slot | Offset | Bytes | Contract | +|------------------------|--------------------------------|------|--------|-------|---------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/auction/Auction.sol:Auction | +| _initializing | bool | 0 | 1 | 1 | src/auction/Auction.sol:Auction | +| _owner | address | 0 | 2 | 20 | src/auction/Auction.sol:Auction | +| _pendingOwner | address | 1 | 0 | 20 | src/auction/Auction.sol:Auction | +| _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | +| _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | +| settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | +| token | contract IBaseToken | 6 | 0 | 20 | src/auction/Auction.sol:Auction | +| auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | +| currentBidReferral | address | 10 | 0 | 20 | src/auction/Auction.sol:Auction | +| founderRewardsRecipent | address | 11 | 0 | 20 | src/auction/Auction.sol:Auction | +| founderRewardBPS | uint256 | 12 | 0 | 32 | src/auction/Auction.sol:Auction | ======================= ➡ Governor diff --git a/src/lib/interfaces/IERC6551Registry.sol b/src/lib/interfaces/IERC6551Registry.sol new file mode 100644 index 0000000..53959f7 --- /dev/null +++ b/src/lib/interfaces/IERC6551Registry.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC6551Registry { + /** + * @dev The registry SHALL emit the AccountCreated event upon successful account creation + */ + event AccountCreated( + address account, + address indexed implementation, + uint256 chainId, + address indexed tokenContract, + uint256 indexed tokenId, + uint256 salt + ); + + /** + * @dev Creates a token bound account for a non-fungible token + * + * If account has already been created, returns the account address without calling create2 + * + * If initData is not empty and account has not yet been created, calls account with + * provided initData after creation + * + * Emits AccountCreated event + * + * @return the address of the account + */ + function createAccount( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 seed, + bytes calldata initData + ) external returns (address); + + /** + * @dev Returns the computed token bound account address for a non-fungible token + * + * @return The computed address of the token bound account + */ + function account( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 salt + ) external view returns (address); +} diff --git a/src/lib/interfaces/IERC721Votes.sol b/src/lib/interfaces/IERC721Votes.sol index 40327b9..d0c9748 100644 --- a/src/lib/interfaces/IERC721Votes.sol +++ b/src/lib/interfaces/IERC721Votes.sol @@ -73,4 +73,11 @@ interface IERC721Votes is IERC721, IEIP712 { bytes32 r, bytes32 s ) external; + + function batchDelegateBySigERC1271( + address[] calldata _fromAddresses, + address _toAddress, + uint256 _deadline, + bytes memory _signature + ) external; } diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index 7fb7f30..e8d2e8d 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -22,7 +22,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)"); /// @dev The EIP-712 typehash to batch delegate with a signature - bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address[] to,uint256[] nonce,uint256[] deadline)"); + bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address to,uint256[] nonce,uint256 deadline)"); /// /// /// STORAGE /// @@ -59,20 +59,20 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { function getBatchDelegateBySigTypedDataHash( address[] calldata _fromAddresses, - address[] calldata _toAddresses, - uint256[] calldata _deadlines + address _toAddress, + uint256 _deadline ) public view returns (bytes32) { uint256 length = _fromAddresses.length; + // Ensure the signature has not expired + if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); + // Cannot realistically overflow unchecked { // Store nonces for each from address uint256[] memory currentNonces = new uint256[](length); for (uint256 i = 0; i < length; ++i) { - // Ensure the signature has not expired - if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE(); - // Add the addresses current nonce to the list of nonces currentNonces[i] = nonces[_fromAddresses[i]]; } @@ -87,9 +87,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { abi.encode( BATCH_DELEGATION_TYPEHASH, keccak256(abi.encodePacked(_fromAddresses)), - keccak256(abi.encodePacked(_toAddresses)), + keccak256(abi.encodePacked(_toAddress)), keccak256(abi.encodePacked(currentNonces)), - keccak256(abi.encodePacked(_deadlines)) + keccak256(abi.encodePacked(_deadline)) ) ) ) @@ -219,8 +219,8 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { function batchDelegateBySigERC1271( address[] calldata _fromAddresses, - address[] calldata _toAddresses, - uint256[] calldata _deadlines, + address _toAddress, + uint256 _deadline, bytes memory _signature ) external { uint256 length = _fromAddresses.length; @@ -228,15 +228,15 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { // Used to store the digest bytes32 digest; + // Ensure the signature has not expired + if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); + // Cannot realistically overflow unchecked { // Store nonces for each from address uint256[] memory currentNonces = new uint256[](length); for (uint256 i = 0; i < length; ++i) { - // Ensure the signature has not expired - if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE(); - // Add the addresses current nonce to the list of nonces currentNonces[i] = nonces[_fromAddresses[i]]++; } @@ -250,9 +250,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { abi.encode( BATCH_DELEGATION_TYPEHASH, keccak256(abi.encodePacked(_fromAddresses)), - keccak256(abi.encodePacked(_toAddresses)), + keccak256(abi.encodePacked(_toAddress)), keccak256(abi.encodePacked(currentNonces)), - keccak256(abi.encodePacked(_deadlines)) + keccak256(abi.encodePacked(_deadline)) ) ) ) @@ -269,7 +269,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { // Ensure the signature is valid if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) { // Update the delegate - _delegate(cachedFromAddress, _toAddresses[i]); + _delegate(cachedFromAddress, _toAddress); } else { revert INVALID_SIGNATURE(); } diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol new file mode 100644 index 0000000..e0f7f3e --- /dev/null +++ b/src/minters/CollectionPlusMinter.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IERC721 } from "../lib/interfaces/IERC721.sol"; +import { IERC6551Registry } from "../lib/interfaces/IERC6551Registry.sol"; +import { IPartialSoulboundToken } from "../token/partial-soulbound/IPartialSoulboundToken.sol"; +import { IManager } from "../manager/IManager.sol"; +import { IOwnable } from "../lib/interfaces/IOwnable.sol"; + +/// @title CollectionPlusMinter +/// @notice A mints and locks reserved tokens to ERC6551 accounts +/// @author @neokry +contract CollectionPlusMinter { + /// @notice General collection plus settings + struct CollectionPlusSettings { + /// @notice Unix timestamp for the mint start + uint64 mintStart; + /// @notice Unix timestamp for the mint end + uint64 mintEnd; + /// @notice Price per token + uint64 pricePerToken; + /// @notice Redemption token + address redeemToken; + } + + /// @notice Event for mint settings updated + event MinterSet(address indexed mediaContract, CollectionPlusSettings merkleSaleSettings); + + error NOT_TOKEN_OWNER(); + error NOT_MANAGER_OWNER(); + error TRANSFER_FAILED(); + error INVALID_OWNER(); + error MINT_ENDED(); + error MINT_NOT_STARTED(); + error INVALID_VALUE(); + + /// @notice Per token mint fee sent to BuilderDAO + uint256 public constant BUILDER_DAO_FEE = 0.000777 ether; + + /// @notice Manager contract + IManager immutable manager; + + /// @notice ERC6551 registry + IERC6551Registry immutable erc6551Registry; + + /// @notice Address to send BuilderDAO fees + address immutable builderFundsRecipent; + + /// @notice Address of the ERC6551 implementation + address immutable erc6551Impl; + + /// @notice Stores the collection plus settings for a token + mapping(address => CollectionPlusSettings) public allowedCollections; + + constructor( + IManager _manager, + IERC6551Registry _erc6551Registry, + address _erc6551Impl, + address _builderFundsRecipent + ) { + manager = _manager; + erc6551Registry = _erc6551Registry; + builderFundsRecipent = _builderFundsRecipent; + erc6551Impl = _erc6551Impl; + } + + /// @notice gets the total fees for minting + function getTotalFeesForMint(address tokenContract, uint256 quantity) public view returns (uint256) { + return _getTotalFeesForMint(allowedCollections[tokenContract].pricePerToken, quantity); + } + + /// @notice mints a token from reserve using the collection plus strategy and sets delegations + /// @param tokenContract The DAO token contract to mint from + /// @param redeemFor Address to redeem tokens for + /// @param tokenIds List of tokenIds to redeem + /// @param initData ERC6551 account init data + /// @param signature ERC1271 signature for delegation + /// @param deadline Deadline for signature + function mintFromReserveAndDelegate( + address tokenContract, + address redeemFor, + uint256[] calldata tokenIds, + bytes calldata initData, + bytes calldata signature, + uint256 deadline + ) public payable { + CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + uint256 tokenCount = tokenIds.length; + + _validateParams(settings, tokenCount); + + address[] memory fromAddresses = new address[](tokenCount); + + unchecked { + for (uint256 i = 0; i < tokenCount; ++i) { + fromAddresses[i] = erc6551Registry.createAccount(erc6551Impl, block.chainid, settings.redeemToken, tokenIds[i], 0, initData); + IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(fromAddresses[i], tokenIds[i]); + + if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { + revert INVALID_OWNER(); + } + } + } + + IPartialSoulboundToken(tokenContract).batchDelegateBySigERC1271(fromAddresses, redeemFor, deadline, signature); + + if (settings.pricePerToken > 0) { + _distributeFees(tokenContract, tokenCount); + } + } + + /// @notice mints a token from reserve using the collection plus strategy + /// @notice mints a token from reserve using the collection plus strategy and sets delegations + /// @param tokenContract The DAO token contract to mint from + /// @param redeemFor Address to redeem tokens for + /// @param tokenIds List of tokenIds to redeem + /// @param initData ERC6551 account init data + function mintFromReserve( + address tokenContract, + address redeemFor, + uint256[] calldata tokenIds, + bytes calldata initData + ) public payable { + CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + uint256 tokenCount = tokenIds.length; + + _validateParams(settings, tokenCount); + + unchecked { + for (uint256 i = 0; i < tokenCount; ++i) { + address account = erc6551Registry.createAccount(erc6551Impl, block.chainid, settings.redeemToken, tokenIds[i], 0, initData); + IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(account, tokenIds[i]); + + if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { + revert INVALID_OWNER(); + } + } + } + + if (settings.pricePerToken > 0) { + _distributeFees(tokenContract, tokenCount); + } + } + + /// @notice Sets the minter settings for a token + /// @param tokenContract Token contract to set settings for + /// @param collectionPlusSettings Settings to set + function setSettings(address tokenContract, CollectionPlusSettings memory collectionPlusSettings) external { + if (IOwnable(tokenContract).owner() != msg.sender) { + revert NOT_TOKEN_OWNER(); + } + + allowedCollections[tokenContract] = collectionPlusSettings; + + // Emit event for new settings + emit MinterSet(tokenContract, collectionPlusSettings); + } + + /// @notice Resets the minter settings for a token + /// @param tokenContract Token contract to reset settings for + function resetSettings(address tokenContract) external { + if (IOwnable(tokenContract).owner() != msg.sender) { + revert NOT_TOKEN_OWNER(); + } + + delete allowedCollections[tokenContract]; + + // Emit event with null settings + emit MinterSet(tokenContract, allowedCollections[tokenContract]); + } + + function _getTotalFeesForMint(uint256 pricePerToken, uint256 quantity) internal pure returns (uint256) { + return pricePerToken > 0 ? quantity * (pricePerToken + BUILDER_DAO_FEE) : 0; + } + + function _validateParams(CollectionPlusSettings memory settings, uint256 tokenCount) internal { + // Check sale end + if (block.timestamp > settings.mintEnd) { + revert MINT_ENDED(); + } + + // Check sale start + if (block.timestamp < settings.mintStart) { + revert MINT_NOT_STARTED(); + } + + if (msg.value < _getTotalFeesForMint(settings.pricePerToken, tokenCount)) { + revert INVALID_VALUE(); + } + } + + function _distributeFees(address tokenContract, uint256 quantity) internal { + uint256 builderFee = quantity * BUILDER_DAO_FEE; + uint256 value = msg.value; + + (, , address treasury, ) = manager.getAddresses(tokenContract); + + (bool builderSuccess, ) = builderFundsRecipent.call{ value: builderFee }(""); + if (!builderSuccess) { + revert TRANSFER_FAILED(); + } + + if (value > builderFee) { + (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); + + if (!builderSuccess || !treasurySuccess) { + revert TRANSFER_FAILED(); + } + } + } +} diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol new file mode 100644 index 0000000..2ddb161 --- /dev/null +++ b/src/minters/MerkleReserveMinter.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import { IOwnable } from "../lib/interfaces/IOwnable.sol"; +import { IToken } from "../token/default/IToken.sol"; +import { IManager } from "../manager/IManager.sol"; + +/// @title MerkleReserveMinter +/// @notice Mints reserved tokens based on a merkle tree +/// @author @neokry +contract MerkleReserveMinter { + /// @notice General merkle sale settings + struct MerkleMinterSettings { + /// @notice Unix timestamp for the mint start + uint64 mintStart; + /// @notice Unix timestamp for the mint end + uint64 mintEnd; + /// @notice Price per token + uint64 pricePerToken; + /// @notice Merkle root for + bytes32 merkleRoot; + } + + /// @notice Parameters for merkle minting + struct MerkleClaim { + /// @notice Address to mint to + address mintTo; + /// @notice Token ID to mint + uint256 tokenId; + /// @notice Merkle proof for token + bytes32[] merkleProof; + } + + /// @notice Event for mint settings updated + event MinterSet(address indexed mediaContract, MerkleMinterSettings merkleSaleSettings); + + /// @notice Manager contract + IManager immutable manager; + + /// @notice Mapping of DAO token contract to merkle settings + mapping(address => MerkleMinterSettings) public allowedMerkles; + + error NOT_TOKEN_OWNER(); + error TRANSFER_FAILED(); + error MINT_ENDED(); + error MINT_NOT_STARTED(); + error INVALID_VALUE(); + error INVALID_CLAIM_COUNT(); + error INVALID_MERKLE_PROOF(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + + /// @notice Checks if the caller is the contract owner + /// @param tokenContract Token contract to check + modifier onlyContractOwner(address tokenContract) { + if (!_isContractOwner(tokenContract)) { + revert NOT_TOKEN_OWNER(); + } + _; + } + + constructor(IManager _manager) { + manager = _manager; + } + + /// @notice Mints tokens from reserve using a merkle proof + /// @param tokenContract Address of token contract + /// @param claims List of merkle claims + function mintFromReserve(address tokenContract, MerkleClaim[] calldata claims) public payable { + MerkleMinterSettings memory settings = allowedMerkles[tokenContract]; + uint256 claimCount = claims.length; + + if (claimCount == 0) { + revert INVALID_CLAIM_COUNT(); + } + + // Check sale end + if (block.timestamp > settings.mintEnd) { + revert MINT_ENDED(); + } + + // Check sale start + if (block.timestamp < settings.mintStart) { + revert MINT_NOT_STARTED(); + } + + if (claimCount * settings.pricePerToken != msg.value) { + revert INVALID_VALUE(); + } + + // Mint tokens + unchecked { + for (uint256 i = 0; i < claimCount; ++i) { + MerkleClaim memory claim = claims[i]; + + if (!MerkleProof.verify(claim.merkleProof, settings.merkleRoot, keccak256(abi.encode(claim.mintTo, claim.tokenId)))) { + revert INVALID_MERKLE_PROOF(claim.mintTo, claim.merkleProof, settings.merkleRoot); + } + + IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); + } + } + + // Transfer funds to treasury + if (settings.pricePerToken > 0) { + (, , address treasury, ) = manager.getAddresses(tokenContract); + + (bool success, ) = treasury.call{ value: msg.value }(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } + } + + /// @notice Sets the minter settings for a token + /// @param tokenContract Token contract to set settings for + /// @param merkleMinterSettings Settings to set + function setSettings(address tokenContract, MerkleMinterSettings memory merkleMinterSettings) external onlyContractOwner(tokenContract) { + allowedMerkles[tokenContract] = merkleMinterSettings; + + // Emit event for new settings + emit MinterSet(tokenContract, merkleMinterSettings); + } + + /// @notice Resets the minter settings for a token + /// @param tokenContract Token contract to reset settings for + function resetSettings(address tokenContract) external onlyContractOwner(tokenContract) { + delete allowedMerkles[tokenContract]; + + // Emit event with null settings + emit MinterSet(tokenContract, allowedMerkles[tokenContract]); + } + + function _isContractOwner(address tokenContract) internal view returns (bool) { + return IOwnable(tokenContract).owner() == msg.sender; + } +} diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index 559da75..387a7f4 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -104,10 +104,22 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + /// @notice Mints the specified token from the reserve to the recipent + function mintFromReserveTo(address recipient, uint256 tokenId) external; + + /// @notice Mints a token from the reserve and locks to the recipient + function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external; + /// @notice Burns a token owned by the caller /// @param tokenId The ERC-721 token id function burn(uint256 tokenId) external; + function transferFromAndLock( + address from, + address to, + uint256 tokenId + ) external; + /// @notice The URI for a token /// @param tokenId The ERC-721 token id function tokenURI(uint256 tokenId) external view returns (string memory); diff --git a/test/CollectionPlusMinter.t.sol b/test/CollectionPlusMinter.t.sol new file mode 100644 index 0000000..f9d22e0 --- /dev/null +++ b/test/CollectionPlusMinter.t.sol @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MockERC6551Registry } from "./utils/mocks/MockERC6551Registry.sol"; +import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; +import { MockERC721 } from "./utils/mocks/MockERC721.sol"; +import { CollectionPlusMinter } from "../src/minters/CollectionPlusMinter.sol"; +import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; + +contract MerkleReserveMinterTest is NounsBuilderTest { + MockERC6551Registry public erc6551Registry; + + CollectionPlusMinter public minter; + PartialSoulboundToken soulboundToken; + MockERC721 public redeemToken; + + address public soulboundTokenImpl; + address public erc6551Impl; + + address internal claimer; + uint256 internal claimerPK; + + function setUp() public virtual override { + super.setUp(); + createClaimer(); + + erc6551Impl = address(0x6551); + redeemToken = new MockERC721(); + + erc6551Registry = new MockERC6551Registry(claimer); + minter = new CollectionPlusMinter(manager, erc6551Registry, erc6551Impl, builderDAO); + } + + function createClaimer() internal { + claimerPK = 0xABE; + claimer = vm.addr(claimerPK); + + vm.deal(claimer, 100 ether); + } + + function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserve(_reservedUntilTokenId); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + + vm.startPrank(zoraDAO); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + vm.stopPrank(); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + + deploy(foundersArr, implAddresses, implData); + + soulboundToken = PartialSoulboundToken(address(token)); + + setMockMetadata(); + } + + function test_MintFlow() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 6; + + minter.mintFromReserve(address(token), claimer, tokenIds, ""); + + address tokenBoundAccount = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + + assertEq(soulboundToken.ownerOf(6), tokenBoundAccount); + assertEq(soulboundToken.locked(6), true); + assertEq(token.getVotes(tokenBoundAccount), 1); + } + + function test_ResetSettings() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + vm.startPrank(address(founder)); + minter.setSettings(address(token), settings); + minter.resetSettings(address(token)); + vm.stopPrank(); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.allowedCollections(address(token)); + assertEq(mintStart, 0); + assertEq(mintEnd, 0); + assertEq(pricePerToken, 0); + assertEq(redeem, address(0)); + } + + function test_MintFlowWithDelegation() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 6; + + address[] memory fromAddresses = new address[](1); + fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + + uint256 deadline = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, claimer, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(claimerPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertEq(token.getVotes(claimer), 0); + + minter.mintFromReserveAndDelegate(address(token), claimer, tokenIds, "", signature, deadline); + + assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); + assertEq(soulboundToken.locked(6), true); + assertEq(token.getVotes(claimer), 1); + } + + function test_MintFlowMultipleTokens() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + minter.mintFromReserve(address(token), claimer, tokenIds, ""); + + address tokenBoundAccount1 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + address tokenBoundAccount2 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); + + assertEq(soulboundToken.ownerOf(6), tokenBoundAccount1); + assertEq(soulboundToken.locked(6), true); + + assertEq(soulboundToken.ownerOf(7), tokenBoundAccount2); + assertEq(soulboundToken.locked(7), true); + + assertEq(token.getVotes(tokenBoundAccount1), 1); + assertEq(token.getVotes(tokenBoundAccount2), 1); + } + + function test_MintFlowMultipleTokensWithDelegation() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + fromAddresses[1] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); + + uint256 deadline = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, claimer, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(claimerPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertEq(token.getVotes(claimer), 0); + + minter.mintFromReserveAndDelegate(address(token), claimer, tokenIds, "", signature, deadline); + + assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); + assertEq(soulboundToken.locked(6), true); + + assertEq(soulboundToken.ownerOf(7), fromAddresses[1]); + assertEq(soulboundToken.locked(7), true); + + assertEq(token.getVotes(claimer), 2); + } + + function test_MintFlowWithFees() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); + + uint256 prevTreasuryBalance = address(treasury).balance; + uint256 prevBuilderBalance = address(builderDAO).balance; + uint256 builderFee = minter.BUILDER_DAO_FEE() * tokenIds.length; + + minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); + + assertEq(address(builderDAO).balance, prevBuilderBalance + builderFee); + assertEq(address(treasury).balance, prevTreasuryBalance + (fees - builderFee)); + } + + function testRevert_MintNotStarted() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: uint64(block.timestamp + 999), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); + + vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); + minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); + } + + function testRevert_MintEnded() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: uint64(block.timestamp), + mintEnd: uint64(block.timestamp + 1), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + vm.warp(block.timestamp + 2); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); + + vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); + minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); + } + + function testRevert_InvalidValue() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: uint64(block.timestamp), + mintEnd: uint64(block.timestamp + 100), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + vm.warp(block.timestamp + 2); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); + minter.mintFromReserve{ value: 0.0001 ether }(address(token), claimer, tokenIds, ""); + } +} diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol new file mode 100644 index 0000000..6303810 --- /dev/null +++ b/test/MerkleReserveMinter.t.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; + +contract MerkleReserveMinterTest is NounsBuilderTest { + MerkleReserveMinter public minter; + + address internal claimer1; + address internal claimer2; + + function setUp() public virtual override { + super.setUp(); + + minter = new MerkleReserveMinter(manager); + claimer1 = address(0xC1); + claimer2 = address(0xC2); + } + + function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserve(_reservedUntilTokenId); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + + function test_MintFlow() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + assertEq(mintStart, settings.mintStart); + assertEq(mintEnd, settings.mintEnd); + assertEq(pricePerToken, settings.pricePerToken); + assertEq(merkleRoot, settings.merkleRoot); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + minter.mintFromReserve(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + } + + function test_MintFlowWithValue() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.deal(claimer1, 0.5 ether); + vm.prank(claimer1); + minter.mintFromReserve{ value: 0.5 ether }(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + assertEq(address(treasury).balance, 0.5 ether); + } + + function test_MintFlowWithValueMultipleTokens() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + vm.deal(claimer1, 1 ether); + vm.prank(claimer1); + minter.mintFromReserve{ value: 1 ether }(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + assertEq(token.ownerOf(6), claimer2); + assertEq(address(treasury).balance, 1 ether); + } + + function testRevert_InvalidValue() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + vm.deal(claimer1, 1 ether); + vm.prank(claimer1); + vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); + minter.mintFromReserve{ value: 0.5 ether }(address(token), claims); + } + + function testRevert_MintNotStarted() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: uint64(block.timestamp + 999), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); + minter.mintFromReserve(address(token), claims); + } + + function testRevert_MintEnded() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: uint64(0), + mintEnd: uint64(1), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.warp(3); + vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); + minter.mintFromReserve(address(token), claims); + } + + function testRevert_InvalidProof() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: uint64(0), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xf77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.expectRevert(abi.encodeWithSignature("INVALID_MERKLE_PROOF(address,bytes32[],bytes32)", claimer1, proof, root)); + minter.mintFromReserve(address(token), claims); + } + + function test_ResetMint() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + vm.prank(address(founder)); + minter.resetSettings(address(token)); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + assertEq(mintStart, 0); + assertEq(mintEnd, 0); + assertEq(pricePerToken, 0); + assertEq(merkleRoot, bytes32(0)); + } +} diff --git a/test/Token.t.sol b/test/Token.t.sol index 0095f22..07a050d 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -942,15 +942,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(delegator)); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; + uint256 deadline = block.timestamp + 100; - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, block.timestamp + 100); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -962,7 +956,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 2); } @@ -974,15 +968,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(delegator)); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; - - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; + uint256 deadline = block.timestamp + 100; - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -997,7 +985,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("EXPIRED_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } @@ -1009,13 +997,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(delegator)); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; - - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; + uint256 deadline = block.timestamp + 100; bytes memory signature = new bytes(0); @@ -1027,7 +1009,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } @@ -1039,15 +1021,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(1); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; + uint256 deadline = block.timestamp + 100; - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -1060,7 +1036,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } @@ -1072,15 +1048,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(address(1))); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; - - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; + uint256 deadline = block.timestamp + 100; - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -1093,7 +1063,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } diff --git a/test/utils/mocks/MockERC6551Registry.sol b/test/utils/mocks/MockERC6551Registry.sol new file mode 100644 index 0000000..37a03b5 --- /dev/null +++ b/test/utils/mocks/MockERC6551Registry.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IERC6551Registry } from "../../../src/lib/interfaces/IERC6551Registry.sol"; +import { MockERC1271 } from "./MockERC1271.sol"; + +contract MockERC6551Registry is IERC6551Registry { + address immutable owner; + + constructor(address _owner) { + owner = _owner; + } + + function createAccount( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 seed, + bytes calldata initData + ) external override returns (address) { + address accountAddr = getAddress(owner, tokenId); + + return accountAddr.code.length == 0 ? address(new MockERC1271{ salt: bytes32(tokenId) }(owner)) : accountAddr; + } + + function account( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 salt + ) external view override returns (address) { + return getAddress(owner, tokenId); + } + + function getBytecode(address _owner) public pure returns (bytes memory) { + bytes memory bytecode = type(MockERC1271).creationCode; + return abi.encodePacked(bytecode, abi.encode(_owner)); + } + + function getAddress(address _owner, uint256 _salt) public view returns (address) { + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(getBytecode(_owner)))); + return address(uint160(uint256(hash))); + } +}