diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index e0592ce..bb10be1 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -6,11 +6,12 @@ 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"; +import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; /// @title CollectionPlusMinter /// @notice A mint strategy that mints and locks reserved tokens to ERC6551 accounts /// @author @neokry -contract CollectionPlusMinter { +contract CollectionPlusMinter is IMintStrategy { /// /// /// EVENTS /// /// /// @@ -92,6 +93,19 @@ contract CollectionPlusMinter { /// @notice Stores the collection plus settings for a token mapping(address => CollectionPlusSettings) public allowedCollections; + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Checks if the caller is the token contract or the owner of the token contract + /// @param tokenContract Token contract to check + modifier onlyTokenOwner(address tokenContract) { + if (!_isContractOwner(msg.sender, tokenContract)) { + revert NOT_TOKEN_OWNER(); + } + _; + } + /// /// /// CONSTRUCTOR /// /// /// @@ -242,7 +256,7 @@ contract CollectionPlusMinter { // Pay out fees to the Builder DAO (bool builderSuccess, ) = builderFundsRecipent.call{ value: builderFee }(""); - // Sanity check: revert if Builder DAO recipent cannot accept funds + // Revert if Builder DAO recipent cannot accept funds if (!builderSuccess) { revert TRANSFER_FAILED(); } @@ -251,7 +265,7 @@ contract CollectionPlusMinter { if (value > builderFee) { (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); - // Sanity check: revert if treasury cannot accept funds + // Revert if treasury cannot accept funds if (!builderSuccess || !treasurySuccess) { revert TRANSFER_FAILED(); } @@ -262,32 +276,40 @@ contract CollectionPlusMinter { /// SETTINGS /// /// /// + // @notice Sets the minter settings from the token contract with generic data + /// @param data Encoded settings to set + function setMintSettings(bytes calldata data) external { + CollectionPlusSettings memory settings = abi.decode(data, (CollectionPlusSettings)); + _setMintSettings(msg.sender, settings); + } + /// @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(); - } - + /// @param settings Settings to set + function setMintSettings(address tokenContract, CollectionPlusSettings memory settings) external onlyTokenOwner(tokenContract) { // Set new collection settings - allowedCollections[tokenContract] = collectionPlusSettings; - - // Emit event for new settings - emit MinterSet(tokenContract, collectionPlusSettings); + _setMintSettings(tokenContract, settings); } /// @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(); - } - + function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { // Reset collection settings to null delete allowedCollections[tokenContract]; // Emit event with null settings emit MinterSet(tokenContract, allowedCollections[tokenContract]); } + + function _setMintSettings(address tokenContract, CollectionPlusSettings memory settings) internal { + // Set new collection settings + allowedCollections[tokenContract] = settings; + + // Emit event for new settings + emit MinterSet(tokenContract, settings); + } + + function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { + return IOwnable(tokenContract).owner() == caller; + } } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index ec22e80..0b7e6c3 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -5,11 +5,12 @@ 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"; +import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; /// @title MerkleReserveMinter /// @notice A mint strategy that mints reserved tokens based on a merkle tree /// @author @neokry -contract MerkleReserveMinter { +contract MerkleReserveMinter is IMintStrategy { /// /// /// EVENTS /// /// /// @@ -89,10 +90,10 @@ contract MerkleReserveMinter { /// MODIFIERS /// /// /// - /// @notice Checks if the caller is the contract owner + /// @notice Checks if the caller is the token contract or the owner of the token contract /// @param tokenContract Token contract to check - modifier onlyContractOwner(address tokenContract) { - if (!_isContractOwner(tokenContract)) { + modifier onlyTokenOwner(address tokenContract) { + if (!_isContractOwner(msg.sender, tokenContract)) { revert NOT_TOKEN_OWNER(); } _; @@ -167,30 +168,41 @@ contract MerkleReserveMinter { /// Settings /// /// /// + /// @notice Sets the minter settings from the token contract with generic data + /// @param data Encoded settings to set + function setMintSettings(bytes calldata data) external { + MerkleMinterSettings memory settings = abi.decode(data, (MerkleMinterSettings)); + _setMintSettings(msg.sender, settings); + } + /// @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); + /// @param settings Settings to set + function setMintSettings(address tokenContract, MerkleMinterSettings memory settings) external onlyTokenOwner(tokenContract) { + _setMintSettings(tokenContract, settings); } /// @notice Resets the minter settings for a token /// @param tokenContract Token contract to reset settings for - function resetSettings(address tokenContract) external onlyContractOwner(tokenContract) { + function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { delete allowedMerkles[tokenContract]; // Emit event with null settings emit MinterSet(tokenContract, allowedMerkles[tokenContract]); } + function _setMintSettings(address tokenContract, MerkleMinterSettings memory settings) internal { + allowedMerkles[tokenContract] = settings; + + // Emit event for new settings + emit MinterSet(tokenContract, settings); + } + /// /// /// Ownership /// /// /// - function _isContractOwner(address tokenContract) internal view returns (bool) { - return IOwnable(tokenContract).owner() == msg.sender; + function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { + return IOwnable(tokenContract).owner() == caller; } } diff --git a/src/minters/interfaces/IMintStrategy.sol b/src/minters/interfaces/IMintStrategy.sol new file mode 100644 index 0000000..2611d2c --- /dev/null +++ b/src/minters/interfaces/IMintStrategy.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title IMintStrategy +/// @notice The interface for external token minting strategies +/// @author @neokry +interface IMintStrategy { + /// @notice Sets the mint settings for a token + /// @param data The encoded mint settings + function setMintSettings(bytes calldata data) external; +} diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 5a41e55..dc7ab17 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -79,6 +79,10 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 string symbol; /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; + /// @notice The minter a DAO enables by default + address initalMinter; + /// @notice The initilization data for the inital minter + bytes initalMinterData; } /// /// diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 8a97188..24a57ff 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -15,9 +15,10 @@ import { IToken } from "./IToken.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; /// @title Token -/// @author Rohan Kulkarni +/// @author Rohan Kulkarni & Neokry /// @custom:repo github.com/ourzora/nouns-protocol /// @notice A DAO's ERC-721 governance token contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC721Votes, TokenStorageV1, TokenStorageV2, TokenStorageV3 { @@ -100,6 +101,16 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; + + // Check if an inital minter was specified + if (params.initalMinter != address(0)) { + minter[params.initalMinter] = true; + + // Set minter settings if specified + if (params.initalMinterData.length > 0) { + IMintStrategy(params.initalMinter).setMintSettings(params.initalMinterData); + } + } } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -208,7 +219,10 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Mints tokens from the reserve to the recipient function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + // Token must be reserved if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + + // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); } @@ -287,6 +301,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Burns a token owned by the caller /// @param _tokenId The ERC-721 token id function burn(uint256 _tokenId) external onlyAuctionOrMinter { + // Ensure the caller owns the token if (ownerOf(_tokenId) != msg.sender) { revert ONLY_TOKEN_OWNER(); } @@ -295,8 +310,10 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } function _burn(uint256 _tokenId) internal override { + // Call the parent burn function super._burn(_tokenId); + // Reduce the total supply unchecked { --settings.totalSupply; } @@ -421,6 +438,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } } + // Clear values from storage before adding new founders settings.numFounders = 0; settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); @@ -447,6 +465,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC return address(settings.metadataRenderer); } + /// @notice The contract owner function owner() public view override(IToken, Ownable) returns (address) { return super.owner(); } diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index 659c585..416b6a2 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -80,6 +80,10 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P string symbol; /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; + /// @notice The minter a DAO enables by default + address initalMinter; + /// @notice The initilization data for the inital minter + bytes initalMinterData; } /// /// diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol index a75a47a..24005bc 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -15,6 +15,7 @@ import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; /// @title Token /// @author Neokry @@ -110,6 +111,16 @@ contract PartialSoulboundToken is settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; + + // Check if an inital minter was specified + if (params.initalMinter != address(0)) { + minter[params.initalMinter] = true; + + // Set minter settings if specified + if (params.initalMinterData.length > 0) { + IMintStrategy(params.initalMinter).setMintSettings(params.initalMinterData); + } + } } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -218,15 +229,22 @@ contract PartialSoulboundToken is /// @notice Mints tokens from the reserve to the recipient function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + // Token must be reserved if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + + // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); } /// @notice Mints a token from the reserve and locks to the recipient function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + // Token must be reserved if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); + + // Permenantly lock the token _lock(tokenId); emit Locked(tokenId); @@ -307,6 +325,7 @@ contract PartialSoulboundToken is /// @notice Burns a token owned by the caller /// @param _tokenId The ERC-721 token id function burn(uint256 _tokenId) external onlyAuctionOrMinter { + // Ensure the caller owns the token if (ownerOf(_tokenId) != msg.sender) { revert ONLY_TOKEN_OWNER(); } @@ -315,8 +334,10 @@ contract PartialSoulboundToken is } function _burn(uint256 _tokenId) internal override { + // Call the parent burn function super._burn(_tokenId); + // Reduce the total supply unchecked { --settings.totalSupply; } @@ -335,9 +356,13 @@ contract PartialSoulboundToken is address to, uint256 tokenId ) external nonReentrant { + // Only reserved tokends are allowed to be locked if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_LOCKABLE(); + // Call the parent transferFrom function super.transferFrom(from, to, tokenId); + + // Permenantly lock the token _lock(tokenId); emit Locked(tokenId); @@ -477,6 +502,7 @@ contract PartialSoulboundToken is } } + // Clear values from storage before adding new founders settings.numFounders = 0; settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); @@ -503,6 +529,7 @@ contract PartialSoulboundToken is return address(settings.metadataRenderer); } + /// @notice The contract owner function owner() public view override(IPartialSoulboundToken, Ownable) returns (address) { return super.owner(); } @@ -528,6 +555,17 @@ contract PartialSoulboundToken is return minter[_minter]; } + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external { + // Ensure the caller is the contract manager + if (msg.sender != address(manager)) { + revert ONLY_MANAGER(); + } + + settings.metadataRenderer = newRenderer; + } + /// /// /// BEFORE TRANSFER OVERRIDE /// /// /// diff --git a/test/CollectionPlusMinter.t.sol b/test/CollectionPlusMinter.t.sol index f9d22e0..083c90d 100644 --- a/test/CollectionPlusMinter.t.sol +++ b/test/CollectionPlusMinter.t.sol @@ -66,6 +66,36 @@ contract MerkleReserveMinterTest is NounsBuilderTest { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + 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); @@ -79,7 +109,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 6); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -99,7 +129,31 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.getVotes(tokenBoundAccount), 1); } - function test_ResetSettings() public { + function test_MintFlowSetFromToken() public { + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer, 6); + + 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_ResetMintSettings() public { deployAltMock(20); CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ @@ -110,8 +164,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.startPrank(address(founder)); - minter.setSettings(address(token), settings); - minter.resetSettings(address(token)); + minter.setMintSettings(address(token), settings); + minter.resetMintSettings(address(token)); vm.stopPrank(); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.allowedCollections(address(token)); @@ -134,7 +188,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 6); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -178,7 +232,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -219,7 +273,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -269,7 +323,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -307,7 +361,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -341,7 +395,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -375,7 +429,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 6303810..961c67d 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -35,6 +35,26 @@ contract MerkleReserveMinterTest is NounsBuilderTest { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + function test_MintFlow() public { deployAltMock(20); @@ -48,7 +68,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); @@ -73,6 +93,35 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.ownerOf(5), claimer1); } + function test_MintFlowSetFromToken() public { + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(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); + + 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); @@ -86,7 +135,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -121,7 +170,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -161,7 +210,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -198,7 +247,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -229,7 +278,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -261,7 +310,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -292,10 +341,10 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); vm.prank(address(founder)); - minter.resetSettings(address(token)); + minter.resetMintSettings(address(token)); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, 0); diff --git a/test/PartialSoulboundToken.t.sol b/test/PartialSoulboundToken.t.sol index 32f2fa3..0c6b732 100644 --- a/test/PartialSoulboundToken.t.sol +++ b/test/PartialSoulboundToken.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; +import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; @@ -45,6 +46,36 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + 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_MockTokenInit() public { deployAltMock(0); @@ -57,6 +88,14 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalSupply(), 0); } + function test_MockTokenWithMinter() public { + MockMinter minter = new MockMinter(); + deployAltMockAndSetMinter(20, address(minter), hex"112233"); + + assertEq(token.minter(address(minter)), true); + assertEq(minter.data(address(token)), hex"112233"); + } + /// Test that the percentages for founders all ends up as expected function test_FounderShareAllocationFuzz( uint256 f1Percentage, diff --git a/test/Token.t.sol b/test/Token.t.sol index 07a050d..60920e6 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; +import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; @@ -43,6 +44,26 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + function test_MockTokenInit() public { deployMock(); @@ -55,6 +76,22 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalSupply(), 0); } + function test_MockTokenWithMinter() public { + MockMinter minter = new MockMinter(); + deployAltMockAndSetMinter(20, address(minter), new bytes(0)); + + assertEq(token.minter(address(minter)), true); + assertEq(minter.data(address(token)), new bytes(0)); + } + + function test_MockTokenWithMinterAndData() public { + MockMinter minter = new MockMinter(); + deployAltMockAndSetMinter(20, address(minter), hex"112233"); + + assertEq(token.minter(address(minter)), true); + assertEq(minter.data(address(token)), hex"112233"); + } + /// Test that the percentages for founders all ends up as expected function test_FounderShareAllocationFuzz( uint256 f1Percentage, diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index b6bbb75..b7815dc 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -142,7 +142,9 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - 0 + 0, + address(0), + new bytes(0) ); } @@ -154,7 +156,27 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - _reservedUntilTokenId + _reservedUntilTokenId, + address(0), + new bytes(0) + ); + } + + function setMockTokenParamsWithReserveAndMinter( + uint256 _reservedUntilTokenId, + address minter, + bytes memory minterData + ) internal virtual { + setTokenParams( + "Mock Token", + "MOCK", + "This is a mock token", + "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", + "https://nouns.build", + "http://localhost:5000/render", + _reservedUntilTokenId, + minter, + minterData ); } @@ -165,9 +187,17 @@ contract NounsBuilderTest is Test { string memory _contractImage, string memory _contractURI, string memory _rendererBase, - uint256 _reservedUntilTokenId + uint256 _reservedUntilTokenId, + address _initalMinter, + bytes memory _initalMinterData ) internal virtual { - tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol, reservedUntilTokenId: _reservedUntilTokenId }); + tokenParams = IToken.TokenParams({ + name: _name, + symbol: _symbol, + reservedUntilTokenId: _reservedUntilTokenId, + initalMinter: _initalMinter, + initalMinterData: _initalMinterData + }); metadataParams = IPropertyMetadata.PropertyMetadataParams({ description: _description, contractImage: _contractImage, @@ -310,7 +340,7 @@ contract NounsBuilderTest is Test { ) internal { setMockFounderParams(); - setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0); + setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0, address(0), new bytes(0)); setMockAuctionParams(); diff --git a/test/utils/mocks/MockMinter.sol b/test/utils/mocks/MockMinter.sol new file mode 100644 index 0000000..c707dc7 --- /dev/null +++ b/test/utils/mocks/MockMinter.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IMintStrategy } from "../../../src/minters/interfaces/IMintStrategy.sol"; + +contract MockMinter is IMintStrategy { + mapping(address => bytes) public data; + + function setMintSettings(bytes calldata _data) external override { + data[msg.sender] = _data; + } +}