From 43a394ab10cd66d3e312e8173ab96edd7d036701 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 16 May 2024 09:51:36 -0700 Subject: [PATCH] PremintV3 - erc20 premints with generalized minter (#379) * generealized miner * deleted premint erc20 --- .changeset/cold-otters-sniff.md | 106 +++++++++++++ .../delegation/ZoraCreator1155Attribution.sol | 145 +++++------------- .../ZoraCreator1155PremintExecutorImpl.sol | 34 +--- .../ZoraCreator1155PremintExecutorImplLib.sol | 17 +- .../test/premint/PremintERC20.t.sol | 135 +++++++++++----- .../ZoraCreator1155PremintExecutor.t.sol | 4 +- .../protocol-deployments/src/typedData.ts | 10 +- packages/protocol-deployments/src/types.ts | 16 +- .../src/premint/contract-types.ts | 2 +- .../src/premint/premint-client.ts | 8 +- .../shared-contracts/src/entities/Premint.sol | 20 +-- .../src/premint/PremintEncoding.sol | 16 +- 12 files changed, 288 insertions(+), 225 deletions(-) create mode 100644 .changeset/cold-otters-sniff.md diff --git a/.changeset/cold-otters-sniff.md b/.changeset/cold-otters-sniff.md new file mode 100644 index 00000000..52daed33 --- /dev/null +++ b/.changeset/cold-otters-sniff.md @@ -0,0 +1,106 @@ +--- +"@zoralabs/zora-1155-contracts": minor +--- + +`ERC20PremintConfig` replaced by a more general purpose `PremintConfigV3`, which instead of having erc20 premint specific properties, as an abi encoded `premintSalesConfig`, that is passed to the function `setPremintSale` on the corresponding minter contract. + +The new `TokenCreationConfigV3` looks like: + +```solidity +struct TokenCreationConfigV3 { + // Metadata URI for the created token + string tokenURI; + // Max supply of the created token + uint256 maxSupply; + // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + uint32 royaltyBPS; + // The address that the will receive rewards/funds/royalties. + address payoutRecipient; + // The address that referred the creation of the token. + address createReferral; + // The start time of the mint, 0 for immediate. + uint64 mintStart; + // The address of the minter module. + address minter; + // The abi encoded data to be passed to the minter to setup the sales config for the premint. + bytes premintSalesConfig; +} +``` + +where the `premintSalesConfig` is an abi encoded struct that is passed to the minter's function `setPremintSale`: + +```solidity +ERC20Minter.PremintSalesConfig memory premintSalesConfig = ERC20Minter.PremintSalesConfig({ + currency: address(mockErc20), + pricePerToken: 1e18, + maxTokensPerAddress: 5000, + duration: 1000, + payoutRecipient: collector + }); + + +// this would be set as the property `premintSalesConfig` in the `TokenCreationConfigV3` +bytes memory encodedPremintSalesConfig = abi.encode(premintSalesConfig); +``` + +Correspondingly, new minters must implement the new interface `ISetPremintSale` to be compatible with the new `TokenCreationConfigV3`: + +```solidity +interface ISetPremintSale { + function setPremintSale(uint256 tokenId, bytes calldata salesConfig) external; +} + +// example implementation: +contract ERC20Minter is ISetPremintSale { + struct PremintSalesConfig { + address currency; + uint256 pricePerToken; + uint64 maxTokensPerAddress; + uint64 duration; + address payoutRecipient; + } + + function buildSalesConfigForPremint( + PremintSalesConfig memory config + ) public view returns (ERC20Minter.SalesConfig memory) { + uint64 saleStart = uint64(block.timestamp); + uint64 saleEnd = config.duration == 0 + ? type(uint64).max + : saleStart + config.duration; + + return + IERC20Minter.SalesConfig({ + saleStart: saleStart, + saleEnd: saleEnd, + maxTokensPerAddress: config.maxTokensPerAddress, + pricePerToken: config.pricePerToken, + fundsRecipient: config.payoutRecipient, + currency: config.currency + }); + } + + function toSaleConfig( + bytes calldata encodedPremintSalesConfig + ) private returns (IERC20Minter.SalesConfig memory) { + PremintSalesConfig memory premintSalesConfig = abi.decode( + encodedPremintSalesConfig, + (PremintSalesConfig) + ); + + return buildSalesConfigForPremint(premintSalesConfig); + } + + mapping(address => mapping(uint256 => IERC20Minter.SalesConfig)) public sale; + + function setPremintSale( + uint256 tokenId, + bytes calldata premintSalesConfig + ) external override { + IERC20Minter.SalesConfig memory salesConfig = toSaleConfig( + premintSalesConfig + ); + + sale[msg.sender][tokenId] = salesConfig; + } +} +``` diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol index 1b95d8f0..55325bf1 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol @@ -9,9 +9,11 @@ import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/con import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; import {IERC20Minter, ERC20Minter} from "../minters/erc20/ERC20Minter.sol"; +import {IMinterPremintSetup} from "../interfaces/IMinterPremintSetup.sol"; import {IERC1271} from "../interfaces/IERC1271.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {PremintConfig, ContractCreationConfig, TokenCreationConfig, PremintConfigV2, TokenCreationConfigV2, Erc20TokenCreationConfigV1, Erc20PremintConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {PremintConfig, ContractCreationConfig, TokenCreationConfig, PremintConfigV2, TokenCreationConfigV2, TokenCreationConfigV3, PremintConfigV3} from "@zoralabs/shared-contracts/entities/Premint.sol"; library ZoraCreator1155Attribution { string internal constant NAME = "Preminter"; @@ -122,7 +124,7 @@ library ZoraCreator1155Attribution { "CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,address erc20Minter,uint64 mintStart,uint64 mintDuration,uint64 maxTokensPerAddress,address currency,uint256 pricePerToken)" ); - function hashPremint(Erc20PremintConfigV1 memory premintConfig) internal pure returns (bytes32) { + function hashPremint(PremintConfigV3 memory premintConfig) internal pure returns (bytes32) { return keccak256( abi.encode(ATTRIBUTION_DOMAIN_ERC20_V1, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted) @@ -177,27 +179,24 @@ library ZoraCreator1155Attribution { ); } - bytes32 constant TOKEN_DOMAIN_ERC20_V1 = + bytes32 constant TOKEN_DOMAIN_V3 = keccak256( - "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,address erc20Minter,uint64 mintStart,uint64 mintDuration,uint64 maxTokensPerAddress,address currency,uint256 pricePerToken)" + "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,uint64 mintStart,address minter,bytes premintSalesConfig)" ); - function _hashToken(Erc20TokenCreationConfigV1 memory tokenConfig) private pure returns (bytes32) { + function _hashToken(TokenCreationConfigV3 memory tokenConfig) private pure returns (bytes32) { return keccak256( abi.encode( - TOKEN_DOMAIN_ERC20_V1, + TOKEN_DOMAIN_V3, _stringHash(tokenConfig.tokenURI), tokenConfig.maxSupply, tokenConfig.royaltyBPS, tokenConfig.payoutRecipient, tokenConfig.createReferral, - tokenConfig.erc20Minter, tokenConfig.mintStart, - tokenConfig.mintDuration, - tokenConfig.maxTokensPerAddress, - tokenConfig.currency, - tokenConfig.pricePerToken + tokenConfig.minter, + keccak256(tokenConfig.premintSalesConfig) ) ); } @@ -215,15 +214,14 @@ library PremintTokenSetup { uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; /// @notice Build token setup actions for a v3 preminted token - function makeSetupNewTokenCalls(uint256 newTokenId, Erc20TokenCreationConfigV1 memory tokenConfig) internal view returns (bytes[] memory calls) { + function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfigV3 memory tokenConfig) internal view returns (bytes[] memory calls) { + bytes memory setupMinterCall = abi.encodeWithSelector(IMinterPremintSetup.setPremintSale.selector, newTokenId, tokenConfig.premintSalesConfig); + return _buildCalls({ newTokenId: newTokenId, - erc20MinterAddress: tokenConfig.erc20Minter, - currency: tokenConfig.currency, - pricePerToken: tokenConfig.pricePerToken, - maxTokensPerAddress: tokenConfig.maxTokensPerAddress, - mintDuration: tokenConfig.mintDuration, + minter: tokenConfig.minter, + setupMinterCall: setupMinterCall, royaltyBPS: tokenConfig.royaltyBPS, payoutRecipient: tokenConfig.payoutRecipient }); @@ -231,13 +229,17 @@ library PremintTokenSetup { /// @notice Build token setup actions for a v2 preminted token function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfigV2 memory tokenConfig) internal view returns (bytes[] memory calls) { + bytes memory setupMinterCall = abi.encodeWithSelector( + ZoraCreatorFixedPriceSaleStrategy.setSale.selector, + newTokenId, + _buildNewSalesConfig(tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration, tokenConfig.payoutRecipient) + ); + return _buildCalls({ newTokenId: newTokenId, - fixedPriceMinterAddress: tokenConfig.fixedPriceMinter, - pricePerToken: tokenConfig.pricePerToken, - maxTokensPerAddress: tokenConfig.maxTokensPerAddress, - mintDuration: tokenConfig.mintDuration, + minter: tokenConfig.fixedPriceMinter, + setupMinterCall: setupMinterCall, royaltyBPS: tokenConfig.royaltyBPS, payoutRecipient: tokenConfig.payoutRecipient }); @@ -245,13 +247,17 @@ library PremintTokenSetup { /// @notice Build token setup actions for a v1 preminted token function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfig memory tokenConfig) internal view returns (bytes[] memory calls) { + bytes memory setupMinterCall = abi.encodeWithSelector( + ZoraCreatorFixedPriceSaleStrategy.setSale.selector, + newTokenId, + _buildNewSalesConfig(tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration, tokenConfig.royaltyRecipient) + ); + return _buildCalls({ newTokenId: newTokenId, - fixedPriceMinterAddress: tokenConfig.fixedPriceMinter, - pricePerToken: tokenConfig.pricePerToken, - maxTokensPerAddress: tokenConfig.maxTokensPerAddress, - mintDuration: tokenConfig.mintDuration, + minter: tokenConfig.fixedPriceMinter, + setupMinterCall: setupMinterCall, royaltyBPS: tokenConfig.royaltyBPS, payoutRecipient: tokenConfig.royaltyRecipient }); @@ -259,67 +265,17 @@ library PremintTokenSetup { function _buildCalls( uint256 newTokenId, - address erc20MinterAddress, - address currency, - uint256 pricePerToken, - uint64 maxTokensPerAddress, - uint64 mintDuration, - uint32 royaltyBPS, - address payoutRecipient - ) private view returns (bytes[] memory calls) { - calls = new bytes[](3); - - calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, erc20MinterAddress, PERMISSION_BIT_MINTER); - - calls[1] = abi.encodeWithSelector( - IZoraCreator1155.callSale.selector, - newTokenId, - IMinter1155(erc20MinterAddress), - abi.encodeWithSelector( - IERC20Minter.setSale.selector, - newTokenId, - _buildNewERC20SalesConfig(currency, pricePerToken, maxTokensPerAddress, mintDuration, payoutRecipient) - ) - ); - - calls[2] = abi.encodeWithSelector( - IZoraCreator1155.updateRoyaltiesForToken.selector, - newTokenId, - ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: royaltyBPS, royaltyRecipient: payoutRecipient, royaltyMintSchedule: 0}) - ); - } - - function _buildCalls( - uint256 newTokenId, - address fixedPriceMinterAddress, - uint96 pricePerToken, - uint64 maxTokensPerAddress, - uint64 mintDuration, + address minter, + bytes memory setupMinterCall, uint32 royaltyBPS, address payoutRecipient ) private view returns (bytes[] memory calls) { calls = new bytes[](3); - // build array of the calls to make - // get setup actions and invoke them - // set up the sales strategy - // first, grant the fixed price sale strategy minting capabilities on the token - // tokenContract.addPermission(newTokenId, address(fixedPriceMinter), PERMISSION_BIT_MINTER); - calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, fixedPriceMinterAddress, PERMISSION_BIT_MINTER); + calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, minter, PERMISSION_BIT_MINTER); - // set the sales config on that token - calls[1] = abi.encodeWithSelector( - IZoraCreator1155.callSale.selector, - newTokenId, - IMinter1155(fixedPriceMinterAddress), - abi.encodeWithSelector( - ZoraCreatorFixedPriceSaleStrategy.setSale.selector, - newTokenId, - _buildNewSalesConfig(pricePerToken, maxTokensPerAddress, mintDuration, payoutRecipient) - ) - ); + calls[1] = abi.encodeWithSelector(IZoraCreator1155.callSale.selector, newTokenId, IMinter1155(minter), setupMinterCall); - // set the royalty config on that token: calls[2] = abi.encodeWithSelector( IZoraCreator1155.updateRoyaltiesForToken.selector, newTokenId, @@ -327,27 +283,6 @@ library PremintTokenSetup { ); } - function _buildNewERC20SalesConfig( - address currency, - uint256 pricePerToken, - uint64 maxTokensPerAddress, - uint64 duration, - address payoutRecipient - ) private view returns (ERC20Minter.SalesConfig memory) { - uint64 saleStart = uint64(block.timestamp); - uint64 saleEnd = duration == 0 ? type(uint64).max : saleStart + duration; - - return - IERC20Minter.SalesConfig({ - saleStart: saleStart, - saleEnd: saleEnd, - maxTokensPerAddress: maxTokensPerAddress, - pricePerToken: pricePerToken, - fundsRecipient: payoutRecipient, - currency: currency - }); - } - function _buildNewSalesConfig( uint96 pricePerToken, uint64 maxTokensPerAddress, @@ -431,11 +366,11 @@ library DelegatedTokenCreation { ); (params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId); - } else if (premintVersion == PremintEncoding.HASHED_ERC20_VERSION_1) { - Erc20PremintConfigV1 memory premintConfig = abi.decode(premintConfigEncoded, (Erc20PremintConfigV1)); + } else if (premintVersion == PremintEncoding.HASHED_VERSION_3) { + PremintConfigV3 memory premintConfig = abi.decode(premintConfigEncoded, (PremintConfigV3)); creatorAttribution = recoverCreatorAttribution( - PremintEncoding.ERC20_VERSION_1, + PremintEncoding.VERSION_3, ZoraCreator1155Attribution.hashPremint(premintConfig), tokenContract, signature, @@ -456,7 +391,7 @@ library DelegatedTokenCreation { versions = new string[](3); versions[0] = PremintEncoding.VERSION_1; versions[1] = PremintEncoding.VERSION_2; - versions[2] = PremintEncoding.ERC20_VERSION_1; + versions[2] = PremintEncoding.VERSION_3; } function recoverCreatorAttribution( @@ -483,7 +418,7 @@ library DelegatedTokenCreation { } function _recoverDelegatedTokenSetup( - Erc20PremintConfigV1 memory premintConfig, + PremintConfigV3 memory premintConfig, uint256 nextTokenId ) private view returns (DelegatedTokenSetup memory params, bytes[] memory tokenSetupActions) { validatePremint(premintConfig.tokenConfig.mintStart, premintConfig.deleted); diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol index 9bd44938..c13f0583 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol @@ -15,7 +15,7 @@ import {ERC1155DelegationStorageV1} from "../delegation/ERC1155DelegationStorage import {ZoraCreator1155PremintExecutorImplLib, GetOrCreateContractResult} from "./ZoraCreator1155PremintExecutorImplLib.sol"; import {ZoraCreator1155Attribution, DelegatedTokenCreation} from "./ZoraCreator1155Attribution.sol"; import {PremintEncoding, EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; -import {ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult, Erc20PremintConfigV1, Erc20TokenCreationConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult, PremintConfigV3, TokenCreationConfigV3} from "@zoralabs/shared-contracts/entities/Premint.sol"; import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155PremintExecutor.sol"; import {IZoraCreator1155DelegatedCreationLegacy, IHasSupportedPremintSignatureVersions} from "../interfaces/IZoraCreator1155DelegatedCreation.sol"; import {ZoraCreator1155FactoryImpl} from "../factory/ZoraCreator1155FactoryImpl.sol"; @@ -170,9 +170,9 @@ contract ZoraCreator1155PremintExecutorImpl is encodedPremintConfig.premintConfigVersion == PremintEncoding.HASHED_VERSION_2 ) { ZoraCreator1155PremintExecutorImplLib.mintWithEth(tokenContract, encodedPremintConfig.minter, tokenId, quantityToMint, mintArguments); - } else if (encodedPremintConfig.premintConfigVersion == PremintEncoding.HASHED_ERC20_VERSION_1) { + } else if (encodedPremintConfig.premintConfigVersion == PremintEncoding.HASHED_VERSION_3) { ZoraCreator1155PremintExecutorImplLib.performERC20Mint( - encodedPremintConfig.premintConfig, + encodedPremintConfig.minter, quantityToMint, address(tokenContract), tokenId, @@ -192,28 +192,6 @@ contract ZoraCreator1155PremintExecutorImpl is }); } - // @custom:deprecated use premintNewContract instead - function premintErc20V1( - ContractCreationConfig calldata contractConfig, - Erc20PremintConfigV1 calldata premintConfig, - bytes calldata signature, - uint256 quantityToMint, - MintArguments calldata mintArguments, - address firstMinter, - address signerContract - ) external returns (PremintResult memory result) { - return - _premintNewContract( - _withEmptySetup(contractConfig), - PremintEncoding.encodePremintErc20V1(premintConfig), - signature, - quantityToMint, - mintArguments, - firstMinter, - signerContract - ); - } - // @custom:deprecated use premintNewContract instead function premintV2WithSignerContract( ContractCreationConfig calldata contractConfig, @@ -281,7 +259,7 @@ contract ZoraCreator1155PremintExecutorImpl is revert OnlyForAbiDefinition(); } - function premintERC20V1Definition(Erc20PremintConfigV1 memory) external pure { + function premintV3Definition(PremintConfigV3 memory) external pure { revert OnlyForAbiDefinition(); } @@ -348,8 +326,8 @@ contract ZoraCreator1155PremintExecutorImpl is } /// @notice Checks if the signer of a premint is authorized to sign a premint for a given contract. If the contract hasn't been created yet, - /// then the signer is authorized if the signer's address matches contractConfig.contractAdmin. Otherwise, the signer must have the PERMISSION_BIT_MINTER - /// role on the contract + /// then the signer is authorized if the signer's address matches contractConfig.contractAdmin. Otherwise, the signer must be + /// in the list of additional admins /// @param signer The signer of the premint /// @param premintContractConfigContractAdmin If this contract was created via premint, the original contractConfig.contractAdmin. Otherwise, set to address(0) /// @param contractAddress The determinstic 1155 contract address the premint is for diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol index cd290fa1..92686ace 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -import {PremintConfig, ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintResult, MintArguments, Erc20TokenCreationConfigV1, Erc20PremintConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {PremintConfig, ContractCreationConfig, ContractWithAdditionalAdminsCreationConfig, PremintResult, MintArguments, TokenCreationConfigV3, PremintConfigV3} from "@zoralabs/shared-contracts/entities/Premint.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Minter} from "../interfaces/IERC20Minter.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; @@ -12,7 +12,6 @@ import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155Pre import {IZoraCreator1155DelegatedCreation, IZoraCreator1155DelegatedCreationLegacy, ISupportsAABasedDelegatedTokenCreation} from "../interfaces/IZoraCreator1155DelegatedCreation.sol"; import {EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; import {IMintWithRewardsRecipients} from "../interfaces/IMintWithRewardsRecipients.sol"; -import {IERC20Minter} from "../interfaces/IERC20Minter.sol"; interface ILegacyZoraCreator1155DelegatedMinter { function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature, address sender) external returns (uint256 newTokenId); @@ -151,16 +150,10 @@ library ZoraCreator1155PremintExecutorImplLib { } } - function performERC20Mint( - bytes memory encodePremintConfig, - uint256 quantityToMint, - address contractAddress, - uint256 tokenId, - MintArguments memory mintArguments - ) internal { - Erc20TokenCreationConfigV1 memory tokenConfig = abi.decode(encodePremintConfig, (Erc20PremintConfigV1)).tokenConfig; + function performERC20Mint(address minter, uint256 quantityToMint, address contractAddress, uint256 tokenId, MintArguments memory mintArguments) internal { + IERC20Minter.SalesConfig memory salesConfig = IERC20Minter(minter).sale(contractAddress, tokenId); - _performERC20Mint(tokenConfig.erc20Minter, tokenConfig.currency, tokenConfig.pricePerToken, quantityToMint, contractAddress, tokenId, mintArguments); + _performERC20Mint(minter, salesConfig.currency, salesConfig.pricePerToken, quantityToMint, contractAddress, tokenId, mintArguments); } function _performERC20Mint( @@ -188,7 +181,7 @@ library ZoraCreator1155PremintExecutorImplLib { IERC20(currency).approve(erc20Minter, totalValue); - IERC20Minter(erc20Minter).mint( + IERC20Minter(erc20Minter).mint{value: msg.value}( mintArguments.mintRecipient, quantityToMint, contractAddress, diff --git a/packages/1155-contracts/test/premint/PremintERC20.t.sol b/packages/1155-contracts/test/premint/PremintERC20.t.sol index cb54c785..e8a954b3 100644 --- a/packages/1155-contracts/test/premint/PremintERC20.t.sol +++ b/packages/1155-contracts/test/premint/PremintERC20.t.sol @@ -12,14 +12,18 @@ import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; import {IZoraCreator1155PremintExecutor, ZoraCreator1155PremintExecutorImpl} from "../../src/delegation/ZoraCreator1155PremintExecutorImpl.sol"; import {ZoraCreator1155PremintExecutorImplLib} from "../../src/delegation/ZoraCreator1155PremintExecutorImplLib.sol"; -import {ZoraCreator1155Attribution, ContractCreationConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; -import {Erc20TokenCreationConfigV1, Erc20PremintConfigV1, MintArguments} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {ZoraCreator1155Attribution} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {TokenCreationConfigV3, PremintConfigV3, MintArguments, ContractWithAdditionalAdminsCreationConfig} from "@zoralabs/shared-contracts/entities/Premint.sol"; import {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; import {Zora1155PremintExecutor} from "../../src/proxies/Zora1155PremintExecutor.sol"; import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {Zora1155} from "../../src/proxies/Zora1155.sol"; +import {IERC20Minter} from "../../src/interfaces/IERC20Minter.sol"; +import {IMinterPremintSetup} from "../../src/interfaces/IMinterPremintSetup.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; contract PremintERC20Test is Test { uint256 internal creatorPK; @@ -65,82 +69,135 @@ contract PremintERC20Test is Test { } function testPremintERC20() public { - // TODO: fix when we have a way to support payable erc20 mints - vm.skip(true); - ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"}); + ContractWithAdditionalAdminsCreationConfig memory contractConfig = ContractWithAdditionalAdminsCreationConfig({ + contractAdmin: creator, + contractName: "test", + contractURI: "test.uri", + additionalAdmins: new address[](0) + }); + + uint256 zerodTokenId = 0; - Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({ + IERC20Minter.PremintSalesConfig memory premintSalesConfig = IERC20Minter.PremintSalesConfig({ + currency: address(mockErc20), + pricePerToken: 1e18, + maxTokensPerAddress: 5000, + duration: 1000, + fundsRecipient: collector + }); + + TokenCreationConfigV3 memory tokenConfig = TokenCreationConfigV3({ tokenURI: "test.token.uri", maxSupply: 1000, royaltyBPS: 0, payoutRecipient: collector, createReferral: address(0), - erc20Minter: address(erc20Minter), + minter: address(erc20Minter), mintStart: 0, - mintDuration: 0, - maxTokensPerAddress: 0, - currency: address(mockErc20), - pricePerToken: 1e18 + premintSalesConfig: abi.encode(premintSalesConfig) }); - Erc20PremintConfigV1 memory premintConfig = Erc20PremintConfigV1({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false}); + PremintConfigV3 memory premintConfig = PremintConfigV3({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false}); - address contractAddress = premint.getContractAddress(contractConfig); - bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_ERC20_VERSION_1, block.chainid); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); + address contractAddress = premint.getContractWithAdditionalAdminsAddress(contractConfig); + bytes memory signature = signPremint(premintConfig, contractAddress); MintArguments memory mintArguments = MintArguments({mintRecipient: collector, mintComment: "test comment", mintRewardsRecipients: new address[](0)}); - uint256 quantityToMint = 1; - uint256 totalValue = tokenConfig.pricePerToken * quantityToMint; - - vm.deal(collector, ethReward); + uint256 quantityToMint = 3; + uint256 totalValue = premintSalesConfig.pricePerToken * quantityToMint; mockErc20.mint(collector, totalValue); vm.prank(collector); mockErc20.approve(address(premint), totalValue); + uint256 totalEthReward = ethReward * quantityToMint; + + vm.deal(collector, totalEthReward); vm.prank(collector); - premint.premintErc20V1(contractConfig, premintConfig, signature, quantityToMint, mintArguments, collector, address(0)); + // validate that the erc20 minter is called with the correct arguments + vm.expectCall( + address(erc20Minter), + totalEthReward, + abi.encodeCall(erc20Minter.mint, (collector, quantityToMint, contractAddress, 1, totalValue, address(mockErc20), address(0), "test comment")) + ); + premint.premintNewContract{value: totalEthReward}( + contractConfig, + abi.encode(premintConfig), + PremintEncoding.VERSION_3, + signature, + quantityToMint, + mintArguments, + collector, + address(0) + ); + + // validate that the erc20 minter has the proper sales config set + IERC20Minter.SalesConfig memory salesConfig = erc20Minter.sale(contractAddress, 1); + assertEq(salesConfig.saleStart, uint64(block.timestamp)); + assertEq(salesConfig.saleEnd, uint64(block.timestamp) + premintSalesConfig.duration); } function testRevertExecutorMustApproveERC20Transfer() public { - ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"}); + ContractWithAdditionalAdminsCreationConfig memory contractConfig = ContractWithAdditionalAdminsCreationConfig({ + contractAdmin: creator, + contractName: "test", + contractURI: "test.uri", + additionalAdmins: new address[](0) + }); + + IERC20Minter.PremintSalesConfig memory premintSalesConfig = IERC20Minter.PremintSalesConfig({ + currency: address(mockErc20), + pricePerToken: 1e18, + maxTokensPerAddress: 0, + duration: 0, + fundsRecipient: collector + }); - Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({ + TokenCreationConfigV3 memory tokenConfig = TokenCreationConfigV3({ tokenURI: "test.token.uri", maxSupply: 1000, royaltyBPS: 0, payoutRecipient: collector, createReferral: address(0), - erc20Minter: address(erc20Minter), + minter: address(erc20Minter), mintStart: 0, - mintDuration: 0, - maxTokensPerAddress: 0, - currency: address(mockErc20), - pricePerToken: 1e18 + premintSalesConfig: abi.encode(premintSalesConfig) }); - Erc20PremintConfigV1 memory premintConfig = Erc20PremintConfigV1({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false}); + PremintConfigV3 memory premintConfig = PremintConfigV3({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false}); - address contractAddress = premint.getContractAddress(contractConfig); - bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_ERC20_VERSION_1, block.chainid); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); + address contractAddress = premint.getContractWithAdditionalAdminsAddress(contractConfig); + bytes memory signature = signPremint(premintConfig, contractAddress); MintArguments memory mintArguments = MintArguments({mintRecipient: collector, mintComment: "test comment", mintRewardsRecipients: new address[](0)}); uint256 quantityToMint = 1; - uint256 totalValue = tokenConfig.pricePerToken * quantityToMint; + uint256 totalValue = premintSalesConfig.pricePerToken * quantityToMint; mockErc20.mint(collector, totalValue); + uint256 totalEthReward = ethReward * quantityToMint; + + vm.deal(collector, totalEthReward); vm.prank(collector); vm.expectRevert("ERC20: insufficient allowance"); - premint.premintErc20V1(contractConfig, premintConfig, signature, quantityToMint, mintArguments, collector, address(0)); + premint.premintNewContract{value: totalEthReward}( + contractConfig, + abi.encode(premintConfig), + PremintEncoding.VERSION_3, + signature, + quantityToMint, + mintArguments, + collector, + address(0) + ); + } + + function signPremint(PremintConfigV3 memory premintConfig, address contractAddress) public view returns (bytes memory) { + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_VERSION_3, block.chainid); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest); + return abi.encodePacked(r, s, v); } } diff --git a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol index 6f1621e8..15f18526 100644 --- a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol +++ b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -852,7 +852,7 @@ contract ZoraCreator1155PreminterTest is Test { assertEq(supportedVersions.length, 3); assertEq(supportedVersions[0], "1"); assertEq(supportedVersions[1], "2"); - assertEq(supportedVersions[2], "ERC20_1"); + assertEq(supportedVersions[2], "3"); } function test_premintVersion_whenCreated_returnsAllVersion() external { @@ -870,7 +870,7 @@ contract ZoraCreator1155PreminterTest is Test { assertEq(supportedVersions.length, 3); assertEq(supportedVersions[0], "1"); assertEq(supportedVersions[1], "2"); - assertEq(supportedVersions[2], "ERC20_1"); + assertEq(supportedVersions[2], "3"); } function testPremintWithCreateReferral() public { diff --git a/packages/protocol-deployments/src/typedData.ts b/packages/protocol-deployments/src/typedData.ts index cce57937..3c85b8f2 100644 --- a/packages/protocol-deployments/src/typedData.ts +++ b/packages/protocol-deployments/src/typedData.ts @@ -10,9 +10,9 @@ import { zoraCreator1155PremintExecutorImplABI, } from "./generated/wagmi"; import { - Erc20PremintConfigV1, PremintConfigV1, PremintConfigV2, + PremintConfigV3, PremintConfigVersion, PremintConfigWithVersion, } from "./types"; @@ -75,10 +75,10 @@ const encodePremintConfigV2 = (config: PremintConfigV2) => { return encodeAbiParameters(abiItem.inputs, [config]); }; -const encodePremintConfigERC20V1 = (config: Erc20PremintConfigV1) => { +export const encodePremintConfigV3 = (config: PremintConfigV3) => { const abiItem = getAbiItem({ abi: zoraCreator1155PremintExecutorImplABI, - name: "premintERC20V1Definition", + name: "premintV3Definition", }); return encodeAbiParameters(abiItem.inputs, [config]); @@ -94,8 +94,8 @@ export const encodePremintConfig = ({ if (premintConfigVersion === PremintConfigVersion.V2) { return encodePremintConfigV2(premintConfig as PremintConfigV2); } - if (premintConfigVersion === PremintConfigVersion.ERC20V1) { - return encodePremintConfigERC20V1(premintConfig as Erc20PremintConfigV1); + if (premintConfigVersion === PremintConfigVersion.V3) { + return encodePremintConfigV3(premintConfig as PremintConfigV3); } throw new Error("Invalid PremintConfigVersion: " + premintConfigVersion); diff --git a/packages/protocol-deployments/src/types.ts b/packages/protocol-deployments/src/types.ts index e4c810a1..eea376d2 100644 --- a/packages/protocol-deployments/src/types.ts +++ b/packages/protocol-deployments/src/types.ts @@ -5,7 +5,7 @@ import { zoraCreator1155PremintExecutorImplABI } from "./generated/wagmi"; export enum PremintConfigVersion { V1 = "1", V2 = "2", - ERC20V1 = "ERC20_1", + V3 = "3", } export type ContractCreationConfig = AbiParametersToPrimitiveTypes< @@ -27,10 +27,10 @@ export type PremintConfigV2 = AbiParametersToPrimitiveTypes< "premintV2Definition" >["inputs"] >[0]; -export type Erc20PremintConfigV1 = AbiParametersToPrimitiveTypes< +export type PremintConfigV3 = AbiParametersToPrimitiveTypes< ExtractAbiFunction< typeof zoraCreator1155PremintExecutorImplABI, - "premintERC20V1Definition" + "premintV3Definition" >["inputs"] >[0]; @@ -46,7 +46,7 @@ export type PremintConfigForVersion = ? PremintConfigV1 : T extends PremintConfigVersion.V2 ? PremintConfigV2 - : Erc20PremintConfigV1; + : PremintConfigV3; export type PremintConfigWithVersion = { premintConfig: PremintConfigForVersion; @@ -55,25 +55,25 @@ export type PremintConfigWithVersion = { export type PremintConfigAndVersion = | PremintConfigWithVersion | PremintConfigWithVersion - | PremintConfigWithVersion; + | PremintConfigWithVersion; export type PremintConfig = PremintConfigV1 | PremintConfigV2; export type TokenCreationConfigV1 = PremintConfigV1["tokenConfig"]; export type TokenCreationConfigV2 = PremintConfigV2["tokenConfig"]; -export type Erc20TokenCreationConfigV1 = Erc20PremintConfigV1["tokenConfig"]; +export type TokenCreationConfigV3 = PremintConfigV3["tokenConfig"]; export type TokenCreationConfig = | TokenCreationConfigV1 | TokenCreationConfigV2 - | Erc20TokenCreationConfigV1; + | TokenCreationConfigV3; export type PremintConfigForTokenCreationConfig = T extends TokenCreationConfigV1 ? PremintConfigV1 : T extends TokenCreationConfigV2 ? PremintConfigV2 - : Erc20PremintConfigV1; + : PremintConfigV3; export type TokenConfigForVersion = PremintConfigForVersion["tokenConfig"]; diff --git a/packages/protocol-sdk/src/premint/contract-types.ts b/packages/protocol-sdk/src/premint/contract-types.ts index dc8dd4fc..eea9e06a 100644 --- a/packages/protocol-sdk/src/premint/contract-types.ts +++ b/packages/protocol-sdk/src/premint/contract-types.ts @@ -3,5 +3,5 @@ import { PremintConfigVersion as PremintConfigVersionOrig } from "@zoralabs/prot export enum PremintConfigVersion { V1 = PremintConfigVersionOrig.V1, V2 = PremintConfigVersionOrig.V2, - ERC20V1 = PremintConfigVersionOrig.ERC20V1, + V3 = PremintConfigVersionOrig.V3, } diff --git a/packages/protocol-sdk/src/premint/premint-client.ts b/packages/protocol-sdk/src/premint/premint-client.ts index 85c6712f..968a6524 100644 --- a/packages/protocol-sdk/src/premint/premint-client.ts +++ b/packages/protocol-sdk/src/premint/premint-client.ts @@ -97,8 +97,8 @@ const makeTokenConfigWithDefaults = ({ tokenCreationConfig: Partial> & { tokenURI: string }; creatorAccount: Address; }): TokenConfigForVersion => { - if (premintConfigVersion === PremintConfigVersion.ERC20V1) { - throw new Error("ERC20V1 not supported in SDK"); + if (premintConfigVersion === PremintConfigVersion.V3) { + throw new Error("PremintV3 not supported in SDK"); } const fixedPriceMinter = (tokenCreationConfig as TokenCreationConfigV1 | TokenCreationConfigV2) @@ -536,8 +536,8 @@ class PremintClient { const numberToMint = BigInt(mintArguments?.quantityToMint || 1); - if (premintConfigVersion === PremintConfigVersion.ERC20V1) { - throw new Error("ERC20 premint not supported in premint SDK"); + if (premintConfigVersion === PremintConfigVersion.V3) { + throw new Error("PremintV3 not supported in premint SDK"); } const value = ( diff --git a/packages/shared-contracts/src/entities/Premint.sol b/packages/shared-contracts/src/entities/Premint.sol index 9ab21b5e..2a127084 100644 --- a/packages/shared-contracts/src/entities/Premint.sol +++ b/packages/shared-contracts/src/entities/Premint.sol @@ -108,9 +108,9 @@ struct TokenCreationConfigV2 { address createReferral; } -struct Erc20PremintConfigV1 { +struct PremintConfigV3 { // The config for the token to be created - Erc20TokenCreationConfigV1 tokenConfig; + TokenCreationConfigV3 tokenConfig; // Unique id of the token, used to ensure that multiple signatures can't be used to create the same intended token. // only one signature per token id, scoped to the contract hash can be executed. uint32 uid; @@ -120,7 +120,7 @@ struct Erc20PremintConfigV1 { bool deleted; } -struct Erc20TokenCreationConfigV1 { +struct TokenCreationConfigV3 { // Metadata URI for the created token string tokenURI; // Max supply of the created token @@ -131,18 +131,12 @@ struct Erc20TokenCreationConfigV1 { address payoutRecipient; // The address that referred the creation of the token. address createReferral; - // The address of the ERC20 minter module. - address erc20Minter; // The start time of the mint, 0 for immediate. uint64 mintStart; - // The duration of the mint, starting from the first mint of this token. 0 for infinite - uint64 mintDuration; - // Max tokens that can be minted for an address, 0 if unlimited - uint64 maxTokensPerAddress; - // The ERC20 currency address - address currency; - // Price per token in ERC20 currency - uint256 pricePerToken; + // The address of the minter module. + address minter; + // The abi encoded data to be passed to the minter to setup the sales config for the premint. + bytes premintSalesConfig; } struct MintArguments { diff --git a/packages/shared-contracts/src/premint/PremintEncoding.sol b/packages/shared-contracts/src/premint/PremintEncoding.sol index bde2c555..8985249c 100644 --- a/packages/shared-contracts/src/premint/PremintEncoding.sol +++ b/packages/shared-contracts/src/premint/PremintEncoding.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {PremintConfig, PremintConfigV2, Erc20PremintConfigV1, PremintConfigCommon, TokenCreationConfig, TokenCreationConfigV2, Erc20TokenCreationConfigV1} from "../entities/Premint.sol"; +import {PremintConfig, PremintConfigV2, PremintConfigV3, PremintConfigCommon, TokenCreationConfig, TokenCreationConfigV2, TokenCreationConfigV3} from "../entities/Premint.sol"; import {IMinter1155} from "../interfaces/IMinter1155.sol"; import {IZoraCreator1155Errors} from "../interfaces/errors/IZoraCreator1155Errors.sol"; @@ -17,8 +17,8 @@ library PremintEncoding { bytes32 internal constant HASHED_VERSION_1 = keccak256(bytes(VERSION_1)); string internal constant VERSION_2 = "2"; bytes32 internal constant HASHED_VERSION_2 = keccak256(bytes(VERSION_2)); - string internal constant ERC20_VERSION_1 = "ERC20_1"; - bytes32 internal constant HASHED_ERC20_VERSION_1 = keccak256(bytes(ERC20_VERSION_1)); + string internal constant VERSION_3 = "3"; + bytes32 internal constant HASHED_VERSION_3 = keccak256(bytes(VERSION_3)); function encodePremintV1(PremintConfig memory premintConfig) internal pure returns (EncodedPremintConfig memory) { return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_1, premintConfig.uid, premintConfig.tokenConfig.fixedPriceMinter); @@ -28,8 +28,8 @@ library PremintEncoding { return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_2, premintConfig.uid, premintConfig.tokenConfig.fixedPriceMinter); } - function encodePremintErc20V1(Erc20PremintConfigV1 memory premintConfig) internal pure returns (EncodedPremintConfig memory) { - return EncodedPremintConfig(abi.encode(premintConfig), HASHED_ERC20_VERSION_1, premintConfig.uid, premintConfig.tokenConfig.erc20Minter); + function encodePremintErc20V1(PremintConfigV3 memory premintConfig) internal pure returns (EncodedPremintConfig memory) { + return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_3, premintConfig.uid, premintConfig.tokenConfig.minter); } function encodePremintConfig(bytes memory encodedPremintConfig, string calldata premintConfigVersion) internal pure returns (EncodedPremintConfig memory) { @@ -43,10 +43,10 @@ library PremintEncoding { PremintConfigV2 memory config = abi.decode(encodedPremintConfig, (PremintConfigV2)); return EncodedPremintConfig(encodedPremintConfig, hashedVersion, config.uid, config.tokenConfig.fixedPriceMinter); - } else if (hashedVersion == HASHED_ERC20_VERSION_1) { - Erc20PremintConfigV1 memory config = abi.decode(encodedPremintConfig, (Erc20PremintConfigV1)); + } else if (hashedVersion == HASHED_VERSION_3) { + PremintConfigV3 memory config = abi.decode(encodedPremintConfig, (PremintConfigV3)); - return EncodedPremintConfig(encodedPremintConfig, hashedVersion, config.uid, config.tokenConfig.erc20Minter); + return EncodedPremintConfig(encodedPremintConfig, hashedVersion, config.uid, config.tokenConfig.minter); } else { revert IZoraCreator1155Errors.InvalidSignatureVersion(); }