From afe3ce36a9103089505d75d1ba02b5aaa3e7dde9 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Wed, 11 Oct 2023 19:00:05 -0400 Subject: [PATCH] feat: add create referral to token params --- .changeset/violet-starfishes-visit.md | 114 ++++++ .../delegation/ZoraCreator1155Attribution.sol | 362 +++++++++++++++--- .../ZoraCreator1155PremintExecutorImpl.sol | 264 ++++++++----- .../ZoraCreator1155PremintExecutorImplLib.sol | 99 +++++ .../src/interfaces/IZoraCreator1155.sol | 11 +- .../IZoraCreator1155PremintExecutor.sol | 90 +++++ .../src/nft/ZoraCreator1155Impl.sol | 59 ++- .../test/fixtures/Zora1155PremintFixtures.sol | 36 +- .../test/nft/ZoraCreator1155.t.sol | 37 +- .../Zora1155PremintExecutorProxy.t.sol | 23 +- .../ZoraCreator1155PremintExecutor.t.sol | 285 +++++++++----- packages/premint-sdk/src/preminter.test.ts | 9 +- .../package/chainConfigs.ts | 5 + .../script/DeployProxiesToNewChain.s.sol | 2 +- .../src/DeploymentTestingUtils.sol | 42 +- ...raCreator1155PremintExecutorForkTest.t.sol | 7 +- 16 files changed, 1115 insertions(+), 330 deletions(-) create mode 100644 .changeset/violet-starfishes-visit.md create mode 100644 packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol create mode 100644 packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol diff --git a/.changeset/violet-starfishes-visit.md b/.changeset/violet-starfishes-visit.md new file mode 100644 index 000000000..ea27371da --- /dev/null +++ b/.changeset/violet-starfishes-visit.md @@ -0,0 +1,114 @@ +--- +"@zoralabs/zora-1155-contracts": minor +--- + +# Premint v2 + +### New fields on signature + +Adding a new `PremintConfigV2` struct that can be signed, that now contains a `createReferral`. `ZoraCreator1155PremintExecutor` recognizes new version of the `PremintConfig`, and still works with the v1 (legacy) version of the `PremintConfig`. + +Additional changes to the `PremintConfigV2`: +* `tokenConfig.royaltyMintSchedule` has been removed as it is deprecated and no longer recognized by new versions of the 1155 contract +* `tokenConfig.royaltyRecipient` has been renamed to `tokenConfig.payoutRecipient` to better reflect the fact that this address is used to receive creator rewards, secondary royalties, and paid mint funds. This is the address that will be set on the `royaltyRecipient` for the created token on the 1155 contract, which is the address that receives creator rewards and secondary royalties for the token, and on the `fundsRecipient` on the ZoraCreatorFixedPriceSaleStrategy contract for the token, which is the address that receives paid mint funds for the token. + +### New MintArguments on premint functions, specifying `mintRecipient` and `mintReferral` + +`mintReferral` and `mintRecipient` are now specified in the premint functions on the `ZoraCreator1155PremintExecutor`, via the `MintArguments mintArguments` param; new `premintV1` and `premintV2` function now take the `MintArguments` struct as an argument which contains `mintRecipient`, defining which account will receive the minted tokens, `mintComment`, and `mintReferral`, defining which account will receive a mintReferral reward, if any. `mintRecipient` must be specified or else it reverts. + +### New signature validation methods + +ZoraCreator1155PremintExecutor can now validate signatures by passing it the contract address, instead of needing to pass the full contract creation config, enabling it to validate signatures for 1155 contracts that were not created via the premint executor contract. This allows premints signatures to be validated on contracts that have been upgraded to a version that supports premints, and allows premints to be created on contracts that were not created via the premint executor contract. These functions are called `isValidSignatureV1` and `isValidSignatureV2` for v1 and v2 of the premint config structs and signatures correspondingly. + +### Changes to handling of setting of fundsRecipient + +Previously the `fundsRecipient` on the fixeed priced minters sales config for the token was set to the signer of the premint. This has been changed to be set to the `payoutRecipient` of the premint config for v2 of premint config, and to the `royaltyRecipient` of the premint config for v1 of the premint config, for 1155 contracts that are to be newly created, and for existing 1155 contracts that are upgraded to the latest version and execute a v1 of the premint config. + +### Changes to 1155's `delegateSetupNewToken` + +`delegateSetupNewToken` on 1155 contract has been updated to now take an abi encoded premint config, premint config version, and send it to an external library to decode the config, the signer, and setup actions. Previously it took a non-encoded PremintConfig. This new change allows this function signature to support multiple versions of a premint config, while offloading decoding of the config and the corresponding setup actions to the external library. This ultimately allows supporting multiple versions of a premint config and corresponding signature without increasing codespace. + +`PremintConfigV2` are updated to containe `createReferral`, and now look like: +```solidity +struct PremintConfigV2 { + // The config for the token to be created + TokenCreationConfigV2 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; + // Version of this premint, scoped to the uid and contract. Not used for logic in the contract, but used externally to track the newest version + uint32 version; + // If executing this signature results in preventing any signature with this uid from being minted. + bool deleted; +} + +struct TokenCreationConfigV2 { + // Metadata URI for the created token + string tokenURI; + // Max supply of the created token + uint256 maxSupply; + // Max tokens that can be minted for an address, 0 if unlimited + uint64 maxTokensPerAddress; + // Price per token in eth wei. 0 for a free mint. + uint96 pricePerToken; + // The start time of the mint, 0 for immediate. Prevents signatures from being used until the start time. + uint64 mintStart; + // The duration of the mint, starting from the first mint of this token. 0 for infinite + uint64 mintDuration; + // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + uint32 royaltyBPS; + // The address that will receive creatorRewards, secondary royalties, and paid mint funds. This is the address that will be set on the `royaltyRecipient` for the created token on the 1155 contract, which is the address that receives creator rewards and secondary royalties for the token, and on the `fundsRecipient` on the ZoraCreatorFixedPriceSaleStrategy contract for the token, which is the address that receives paid mint funds for the token. + address payoutRecipient; + // Fixed price minter address + address fixedPriceMinter; + // create referral + address createReferral; +} +``` +`PremintConfig` fields are **the same as they were before, but are treated as a version 1**: + +```solidity +struct PremintConfig { + // The config for the token to be created + TokenCreationConfig 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; + // Version of this premint, scoped to the uid and contract. Not used for logic in the contract, but used externally to track the newest version + uint32 version; + // If executing this signature results in preventing any signature with this uid from being minted. + bool deleted; +} + +struct TokenCreationConfig { + // Metadata URI for the created token + string tokenURI; + // Max supply of the created token + uint256 maxSupply; + // Max tokens that can be minted for an address, 0 if unlimited + uint64 maxTokensPerAddress; + // Price per token in eth wei. 0 for a free mint. + uint96 pricePerToken; + // The start time of the mint, 0 for immediate. Prevents signatures from being used until the start time. + uint64 mintStart; + // The duration of the mint, starting from the first mint of this token. 0 for infinite + uint64 mintDuration; + // deperecated field; will be ignored. + uint32 royaltyMintSchedule; + // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + uint32 royaltyBPS; + // The address that will receive creatorRewards, secondary royalties, and paid mint funds. This is the address that will be set on the `royaltyRecipient` for the created token on the 1155 contract, which is the address that receives creator rewards and secondary royalties for the token, and on the `fundsRecipient` on the ZoraCreatorFixedPriceSaleStrategy contract for the token, which is the address that receives paid mint funds for the token. + address royaltyRecipient; + // Fixed price minter address + address fixedPriceMinter; +} +``` + +### changes to `ZoraCreator1155PremintExecutorImpl`: + +* new function `premintV1` - takes a `PremintConfig`, and premint v1 signature, and executes a premint, with added functionality of being able to specify mint referral and mint recipient +* new function `premintV2` - takes a `PremintConfigV2` signature and executes a premint, with being able to specify mint referral and mint recipient +* deprecated function `premint` - call `premintV1` instead +* new function `isValidSignatureV1` - takes an 1155 address, contract admin, premint v1 config and signature, and validates the signature. Can be used for 1155 contracts that were not created via the premint executor contract. +* new function `isValidSignatureV2` - takes an 1155 address, contract admin, premint v2 config and signature, and validates the signature. Can be used for 1155 contracts that were not created via the premint executor contract. +* deprecated function `isValidSignature` - call `isValidSignatureV1` instead \ No newline at end of file diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol index 3249beae2..e2c6ee414 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.17; import {IMinter1155} from "../interfaces/IMinter1155.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; +import {IZoraCreator1155Errors} from "../interfaces/IZoraCreator1155Errors.sol"; import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; @@ -16,6 +17,18 @@ struct ContractCreationConfig { string contractName; } +struct PremintConfig { + // The config for the token to be created + TokenCreationConfig 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; + // Version of this premint, scoped to the uid and contract. Not used for logic in the contract, but used externally to track the newest version + uint32 version; + // If executing this signature results in preventing any signature with this uid from being minted. + bool deleted; +} + struct TokenCreationConfig { // Metadata URI for the created token string tokenURI; @@ -29,19 +42,22 @@ struct TokenCreationConfig { uint64 mintStart; // The duration of the mint, starting from the first mint of this token. 0 for infinite uint64 mintDuration; - // RoyaltyMintSchedule for created tokens. Every nth token will go to the royalty recipient. + // deperecated field; will be ignored. uint32 royaltyMintSchedule; // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. uint32 royaltyBPS; - // RoyaltyRecipient for created tokens. The address that will receive the royalty payments. + // This is the address that will be set on the `royaltyRecipient` for the created token on the 1155 contract, + // which is the address that receives creator rewards and secondary royalties for the token, + // and on the `fundsRecipient` on the ZoraCreatorFixedPriceSaleStrategy contract for the token, + // which is the address that receives paid mint funds for the token. address royaltyRecipient; // Fixed price minter address address fixedPriceMinter; } -struct PremintConfig { +struct PremintConfigV2 { // The config for the token to be created - TokenCreationConfig tokenConfig; + TokenCreationConfigV2 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; @@ -51,94 +67,124 @@ struct PremintConfig { bool deleted; } -/// @title Library for enables a creator to signal intent to create a Zora erc1155 contract or new token on that -/// contract by signing a transaction but not paying gas, and have a third party/collector pay the gas -/// by executing the transaction. Functions are exposed as external to allow contracts to import this lib and not increase their -/// size. -/// @author @oveddan +struct TokenCreationConfigV2 { + // Metadata URI for the created token + string tokenURI; + // Max supply of the created token + uint256 maxSupply; + // Max tokens that can be minted for an address, 0 if unlimited + uint64 maxTokensPerAddress; + // Price per token in eth wei. 0 for a free mint. + uint96 pricePerToken; + // The start time of the mint, 0 for immediate. Prevents signatures from being used until the start time. + uint64 mintStart; + // The duration of the mint, starting from the first mint of this token. 0 for infinite + uint64 mintDuration; + // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + uint32 royaltyBPS; + // This is the address that will be set on the `royaltyRecipient` for the created token on the 1155 contract, + // which is the address that receives creator rewards and secondary royalties for the token, + // and on the `fundsRecipient` on the ZoraCreatorFixedPriceSaleStrategy contract for the token, + // which is the address that receives paid mint funds for the token. + address payoutRecipient; + // Fixed price minter address + address fixedPriceMinter; + // create referral + address createReferral; +} + library ZoraCreator1155Attribution { - /* start eip712 functionality */ string internal constant NAME = "Preminter"; - string internal constant VERSION = "1"; bytes32 internal constant HASHED_NAME = keccak256(bytes(NAME)); - bytes32 internal constant HASHED_VERSION = keccak256(bytes(VERSION)); - bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + string internal constant VERSION_1 = "1"; + 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)); /** * @dev Returns the domain separator for the specified chain. */ - function _domainSeparatorV4(uint256 chainId, address verifyingContract) internal pure returns (bytes32) { - return _buildDomainSeparator(HASHED_NAME, HASHED_VERSION, verifyingContract, chainId); + function _domainSeparatorV4(uint256 chainId, address verifyingContract, bytes32 hashedName, bytes32 hashedVersion) private pure returns (bytes32) { + return _buildDomainSeparator(hashedName, hashedVersion, verifyingContract, chainId); } function _buildDomainSeparator(bytes32 nameHash, bytes32 versionHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) { return keccak256(abi.encode(TYPE_HASH, nameHash, versionHash, chainId, verifyingContract)); } - function _hashTypedDataV4(bytes32 structHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) { - return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract), structHash); - } - - /* end eip712 functionality */ - - function recoverSigner( - PremintConfig calldata premintConfig, - bytes calldata signature, - address erc1155Contract, + function _hashTypedDataV4( + bytes32 structHash, + bytes32 hashedName, + bytes32 hashedVersion, + address verifyingContract, uint256 chainId - ) internal pure returns (address signatory) { - // first validate the signature - the creator must match the signer of the message - return recoverSignerHashed(hashPremint(premintConfig), signature, erc1155Contract, chainId); + ) private pure returns (bytes32) { + return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract, hashedName, hashedVersion), structHash); } function recoverSignerHashed( bytes32 hashedPremintConfig, bytes calldata signature, address erc1155Contract, + bytes32 signatureVersion, uint256 chainId - ) public pure returns (address signatory) { + ) internal pure returns (address signatory) { // first validate the signature - the creator must match the signer of the message - bytes32 digest = _hashTypedDataV4( + bytes32 digest = premintHashedTypeDataV4( hashedPremintConfig, // here we pass the current contract and chain id, ensuring that the message // only works for the current chain and contract id erc1155Contract, + signatureVersion, chainId ); - signatory = ECDSAUpgradeable.recover(digest, signature); + (signatory, ) = ECDSAUpgradeable.tryRecover(digest, signature); } - /// Gets hash data to sign for a premint. Allows specifying a different chain id and contract address so that the signature - /// can be verified on a different chain. + /// Gets hash data to sign for a premint. /// @param erc1155Contract Contract address that signature is to be verified against /// @param chainId Chain id that signature is to be verified on - function premintHashedTypeDataV4(PremintConfig calldata premintConfig, address erc1155Contract, uint256 chainId) external pure returns (bytes32) { + function premintHashedTypeDataV4(bytes32 structHash, address erc1155Contract, bytes32 signatureVersion, uint256 chainId) internal pure returns (bytes32) { // build the struct hash to be signed // here we pass the chain id, allowing the message to be signed for another chain - return _hashTypedDataV4(hashPremint(premintConfig), erc1155Contract, chainId); + return _hashTypedDataV4(structHash, HASHED_NAME, signatureVersion, erc1155Contract, chainId); } - bytes32 constant ATTRIBUTION_DOMAIN = + bytes32 constant ATTRIBUTION_DOMAIN_V1 = keccak256( "CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter)" ); - function hashPremint(PremintConfig calldata premintConfig) public pure returns (bytes32) { + function hashPremint(PremintConfig memory premintConfig) internal pure returns (bytes32) { return - keccak256(abi.encode(ATTRIBUTION_DOMAIN, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted)); + keccak256( + abi.encode(ATTRIBUTION_DOMAIN_V1, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted) + ); } - bytes32 constant TOKEN_DOMAIN = + bytes32 constant ATTRIBUTION_DOMAIN_V2 = + keccak256( + "CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter,address createReferral)" + ); + + function hashPremint(PremintConfigV2 memory premintConfig) internal pure returns (bytes32) { + return + keccak256( + abi.encode(ATTRIBUTION_DOMAIN_V2, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted) + ); + } + + bytes32 constant TOKEN_DOMAIN_V1 = keccak256( "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter)" ); - function _hashToken(TokenCreationConfig calldata tokenConfig) private pure returns (bytes32) { + function _hashToken(TokenCreationConfig memory tokenConfig) private pure returns (bytes32) { return keccak256( abi.encode( - TOKEN_DOMAIN, + TOKEN_DOMAIN_V1, _stringHash(tokenConfig.tokenURI), tokenConfig.maxSupply, tokenConfig.maxTokensPerAddress, @@ -153,9 +199,62 @@ library ZoraCreator1155Attribution { ); } - function _stringHash(string calldata value) private pure returns (bytes32) { + bytes32 constant TOKEN_DOMAIN_V2 = + keccak256( + "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyBPS,address payoutRecipient,address fixedPriceMinter,address createReferral)" + ); + + function _hashToken(TokenCreationConfigV2 memory tokenConfig) private pure returns (bytes32) { + return + keccak256( + abi.encode( + TOKEN_DOMAIN_V1, + _stringHash(tokenConfig.tokenURI), + tokenConfig.maxSupply, + tokenConfig.maxTokensPerAddress, + tokenConfig.pricePerToken, + tokenConfig.mintStart, + tokenConfig.mintDuration, + tokenConfig.royaltyBPS, + tokenConfig.payoutRecipient, + tokenConfig.fixedPriceMinter, + tokenConfig.createReferral + ) + ); + } + + bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + function _stringHash(string memory value) private pure returns (bytes32) { return keccak256(bytes(value)); } + + /// @notice copied from SharedBaseConstants + uint256 constant CONTRACT_BASE_ID = 0; + /// @dev copied from ZoraCreator1155Impl + uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; + + function isValidSignature( + address originalPremintCreator, + address contractAddress, + bytes32 structHash, + bytes32 hashedVersion, + bytes calldata signature + ) internal view returns (bool isValid, address recoveredSigner) { + recoveredSigner = recoverSignerHashed(structHash, signature, contractAddress, hashedVersion, block.chainid); + + if (recoveredSigner == address(0)) { + return (false, address(0)); + } + + // if contract hasn't been created, signer must be the contract admin on the config + if (contractAddress.code.length == 0) { + isValid = recoveredSigner == originalPremintCreator; + } else { + // if contract has been created, signer must have mint new token permission + isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); + } + } } /// @notice Utility library to setup tokens created via premint. Functions exposed as external to not increase contract size in calling contract. @@ -163,14 +262,45 @@ library ZoraCreator1155Attribution { library PremintTokenSetup { uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; - function makeSetupNewTokenCalls( + /// @notice Build token setup actions for a v2 preminted token + function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfigV2 memory tokenConfig) internal view returns (bytes[] memory calls) { + return + _buildCalls({ + newTokenId: newTokenId, + fixedPriceMinterAddress: tokenConfig.fixedPriceMinter, + pricePerToken: tokenConfig.pricePerToken, + maxTokensPerAddress: tokenConfig.maxTokensPerAddress, + mintDuration: tokenConfig.mintDuration, + royaltyBPS: tokenConfig.royaltyBPS, + payoutRecipient: tokenConfig.payoutRecipient + }); + } + + /// @notice Build token setup actions for a v1 preminted token + function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfig memory tokenConfig) internal view returns (bytes[] memory calls) { + return + _buildCalls({ + newTokenId: newTokenId, + fixedPriceMinterAddress: tokenConfig.fixedPriceMinter, + pricePerToken: tokenConfig.pricePerToken, + maxTokensPerAddress: tokenConfig.maxTokensPerAddress, + mintDuration: tokenConfig.mintDuration, + royaltyBPS: tokenConfig.royaltyBPS, + payoutRecipient: tokenConfig.royaltyRecipient + }); + } + + function _buildCalls( uint256 newTokenId, - address contractAdmin, - TokenCreationConfig calldata tokenConfig - ) external view returns (bytes[] memory calls) { + address fixedPriceMinterAddress, + uint96 pricePerToken, + uint64 maxTokensPerAddress, + uint64 mintDuration, + uint32 royaltyBPS, + address payoutRecipient + ) private view returns (bytes[] memory calls) { calls = new bytes[](3); - address fixedPriceMinterAddress = tokenConfig.fixedPriceMinter; // build array of the calls to make // get setup actions and invoke them // set up the sales strategy @@ -186,7 +316,7 @@ library PremintTokenSetup { abi.encodeWithSelector( ZoraCreatorFixedPriceSaleStrategy.setSale.selector, newTokenId, - _buildNewSalesConfig(contractAdmin, tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration) + _buildNewSalesConfig(pricePerToken, maxTokensPerAddress, mintDuration) ) ); @@ -194,16 +324,11 @@ library PremintTokenSetup { calls[2] = abi.encodeWithSelector( IZoraCreator1155.updateRoyaltiesForToken.selector, newTokenId, - ICreatorRoyaltiesControl.RoyaltyConfiguration({ - royaltyBPS: tokenConfig.royaltyBPS, - royaltyRecipient: tokenConfig.royaltyRecipient, - royaltyMintSchedule: tokenConfig.royaltyMintSchedule - }) + ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: royaltyBPS, royaltyRecipient: payoutRecipient, royaltyMintSchedule: 0}) ); } function _buildNewSalesConfig( - address creator, uint96 pricePerToken, uint64 maxTokensPerAddress, uint64 duration @@ -217,7 +342,134 @@ library PremintTokenSetup { saleStart: saleStart, saleEnd: saleEnd, maxTokensPerAddress: maxTokensPerAddress, - fundsRecipient: creator + fundsRecipient: address(0) }); } } + +library PremintEncoding { + function encodePremintV1(PremintConfig memory premintConfig) internal pure returns (bytes memory encodedPremintConfig, bytes32 hashedVersion) { + return (abi.encode(premintConfig), ZoraCreator1155Attribution.HASHED_VERSION_1); + } + + function encodePremintV2(PremintConfigV2 memory premintConfig) internal pure returns (bytes memory encodedPremintConfig, bytes32 hashedVersion) { + return (abi.encode(premintConfig), ZoraCreator1155Attribution.HASHED_VERSION_2); + } +} + +struct DecodedCreatorAttribution { + bytes32 structHash; + string domainName; + string version; + address creator; + bytes signature; +} + +struct DelegatedTokenSetup { + DecodedCreatorAttribution attribution; + uint32 uid; + string tokenURI; + uint256 maxSupply; + address createReferral; +} + +/// @notice Utility library to decode and recover delegated token setup data from a signature. +/// Function called by the erc1155 contract is marked external to reduce contract size in calling contract. +library DelegatedTokenCreation { + /// @notice Decode and recover delegated token setup data from a signature. Works with multiple versions of + /// a signature. Takes an abi encoded premint config, version of the encoded premint config, and a signature, + /// decodes the config, and recoveres the signer of the config. Based on the premint config, builds + /// setup actions for the token to be created. + /// @param premintConfigEncoded The abi encoded premint config + /// @param premintVersion The version of the premint config + /// @param signature The signature of the premint config + /// @param tokenContract The address of the token contract that the premint config is for + /// @param newTokenId The id of the token to be created + function decodeAndRecoverDelegatedTokenSetup( + bytes memory premintConfigEncoded, + bytes32 premintVersion, + bytes calldata signature, + address tokenContract, + uint256 newTokenId + ) external view returns (DelegatedTokenSetup memory params, DecodedCreatorAttribution memory creatorAttribution, bytes[] memory tokenSetupActions) { + // based on version of encoded premint config, decode corresponding premint config, + // and then recover signer from the signature, and then build token setup actions based + // on the decoded premint config. + if (premintVersion == ZoraCreator1155Attribution.HASHED_VERSION_1) { + PremintConfig memory premintConfig = abi.decode(premintConfigEncoded, (PremintConfig)); + + creatorAttribution = _recoverCreatorAttribution( + ZoraCreator1155Attribution.VERSION_1, + ZoraCreator1155Attribution.hashPremint(premintConfig), + tokenContract, + signature + ); + + (params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId); + } else { + PremintConfigV2 memory premintConfig = abi.decode(premintConfigEncoded, (PremintConfigV2)); + + creatorAttribution = _recoverCreatorAttribution( + ZoraCreator1155Attribution.VERSION_2, + ZoraCreator1155Attribution.hashPremint(premintConfig), + tokenContract, + signature + ); + + (params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId); + } + } + + function _recoverCreatorAttribution( + string memory version, + bytes32 structHash, + address tokenContract, + bytes calldata signature + ) private view returns (DecodedCreatorAttribution memory attribution) { + attribution.version = version; + + attribution.creator = ZoraCreator1155Attribution.recoverSignerHashed(structHash, signature, tokenContract, keccak256(bytes(version)), block.chainid); + + attribution.signature = signature; + attribution.domainName = ZoraCreator1155Attribution.NAME; + } + + function _recoverDelegatedTokenSetup( + PremintConfigV2 memory premintConfig, + uint256 nextTokenId + ) private view returns (DelegatedTokenSetup memory params, bytes[] memory tokenSetupActions) { + validatePremint(premintConfig.tokenConfig.mintStart, premintConfig.deleted); + + params.uid = premintConfig.uid; + + tokenSetupActions = PremintTokenSetup.makeSetupNewTokenCalls({newTokenId: nextTokenId, tokenConfig: premintConfig.tokenConfig}); + + params.tokenURI = premintConfig.tokenConfig.tokenURI; + params.maxSupply = premintConfig.tokenConfig.maxSupply; + params.createReferral = premintConfig.tokenConfig.createReferral; + } + + function _recoverDelegatedTokenSetup( + PremintConfig memory premintConfig, + uint256 nextTokenId + ) private view returns (DelegatedTokenSetup memory params, bytes[] memory tokenSetupActions) { + validatePremint(premintConfig.tokenConfig.mintStart, premintConfig.deleted); + + tokenSetupActions = PremintTokenSetup.makeSetupNewTokenCalls(nextTokenId, premintConfig.tokenConfig); + + params.tokenURI = premintConfig.tokenConfig.tokenURI; + params.maxSupply = premintConfig.tokenConfig.maxSupply; + } + + function validatePremint(uint64 mintStart, bool deleted) private view { + if (mintStart != 0 && mintStart > block.timestamp) { + // if the mint start is in the future, then revert + revert IZoraCreator1155Errors.MintNotYetStarted(); + } + if (deleted) { + // if the signature says to be deleted, then dont execute any further minting logic; + // return 0 + revert IZoraCreator1155Errors.PremintDeleted(); + } + } +} diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol index 45805a927..0e5ea5c30 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol @@ -12,20 +12,33 @@ import {SharedBaseConstants} from "../shared/SharedBaseConstants.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {IMinter1155} from "../interfaces/IMinter1155.sol"; import {ERC1155DelegationStorageV1} from "../delegation/ERC1155DelegationStorageV1.sol"; -import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1155Attribution} from "./ZoraCreator1155Attribution.sol"; +import {ZoraCreator1155PremintExecutorImplLib} from "./ZoraCreator1155PremintExecutorImplLib.sol"; +import {PremintEncoding, ZoraCreator1155Attribution, ContractCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2} from "./ZoraCreator1155Attribution.sol"; +import {IZoraCreator1155PremintExecutor, ILegacyZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155PremintExecutor.sol"; + +struct MintArguments { + // which account should receive the tokens minted. If set to address(0), then defaults to the msg.sender + address mintRecipient; + // comment to add to the mint + string mintComment; + // account that referred the minter to mint the tokens, this account will receive a mint referral award. If set to address(0), no account will get the mint referral reward + address mintReferral; +} /// @title Enables creation of and minting tokens on Zora1155 contracts transactions using eip-712 signatures. /// Signature must provided by the contract creator, or an account that's permitted to create new tokens on the contract. /// Mints the first x tokens to the executor of the transaction. /// @author @oveddan -contract ZoraCreator1155PremintExecutorImpl is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName, IZoraCreator1155Errors { +contract ZoraCreator1155PremintExecutorImpl is + ILegacyZoraCreator1155PremintExecutor, + IZoraCreator1155PremintExecutor, + Ownable2StepUpgradeable, + UUPSUpgradeable, + IHasContractName, + IZoraCreator1155Errors +{ IZoraCreator1155Factory public immutable zora1155Factory; - /// @notice copied from SharedBaseConstants - uint256 constant CONTRACT_BASE_ID = 0; - /// @dev copied from ZoraCreator1155Impl - uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; - constructor(IZoraCreator1155Factory _factory) { zora1155Factory = _factory; } @@ -35,107 +48,100 @@ contract ZoraCreator1155PremintExecutorImpl is Ownable2StepUpgradeable, UUPSUpgr __UUPSUpgradeable_init(); } - event Preminted( - address indexed contractAddress, - uint256 indexed tokenId, - bool indexed createdNewContract, - uint32 uid, - ContractCreationConfig contractConfig, - TokenCreationConfig tokenConfig, - address minter, - uint256 quantityMinted - ); - - /// Creates a new token on the given erc1155 contract on behalf of a creator, and mints x tokens to the executor of this transaction. + /// @notice Creates a new token on the given erc1155 contract on behalf of a creator, and mints x tokens to the executor of this transaction. /// If the erc1155 contract hasn't been created yet, it will be created with the given config within this same transaction. /// The creator must sign the intent to create the token, and must have mint new token permission on the erc1155 contract, /// or match the contract admin on the contract creation config if the contract hasn't been created yet. /// Contract address of the created contract is deterministically generated from the contract config and this contract's address. + /// @dev For use with v2 of premint config, PremintConfigV2, which supports setting `createReferral`. /// @param contractConfig Parameters for creating a new contract, if one doesn't exist yet. Used to resolve the deterministic contract address. /// @param premintConfig Parameters for creating the token, and minting the initial x tokens to the executor. /// @param signature Signature of the creator of the token, which must match the signer of the premint config, or have permission to create new tokens on the erc1155 contract if it's already been created - /// @param quantityToMint How many tokens to mint to the executor of this transaction once the token is created - /// @param mintComment A comment to associate with the mint action - function premint( + /// @param quantityToMint How many tokens to mint to the mintRecipient + /// @param mintArguments mint arguments specifying the token mint recipient, mint comment, and mint referral + function premintV2( ContractCreationConfig calldata contractConfig, - PremintConfig calldata premintConfig, + PremintConfigV2 calldata premintConfig, bytes calldata signature, uint256 quantityToMint, - string calldata mintComment - ) public payable returns (uint256 newTokenId) { - // get or create the contract with the given params - // contract address is deterministic. - (IZoraCreator1155 tokenContract, bool isNewContract) = _getOrCreateContract(contractConfig); - - // pass the signature and the premint config to the token contract to create the token. - // The token contract will verify the signature and that the signer has permission to create a new token. - // and then create and setup the token using the given token config. - newTokenId = tokenContract.delegateSetupNewToken(premintConfig, signature, msg.sender); - - // if the executor would also like to mint: - if (quantityToMint != 0) { - // mint the number of specified tokens to the executor - tokenContract.mint{value: msg.value}( - IMinter1155(premintConfig.tokenConfig.fixedPriceMinter), - newTokenId, - quantityToMint, - abi.encode(msg.sender, mintComment) - ); + MintArguments calldata mintArguments + ) external payable returns (PremintResult memory result) { + (bytes memory encodedPremint, bytes32 premintVersion) = PremintEncoding.encodePremintV2(premintConfig); + address fixedPriceMinter = premintConfig.tokenConfig.fixedPriceMinter; + uint32 uid = premintConfig.uid; + + // we wrap this here to get around stack too deep issues + { + result = ZoraCreator1155PremintExecutorImplLib.premint({ + zora1155Factory: zora1155Factory, + contractConfig: contractConfig, + encodedPremintConfig: encodedPremint, + premintVersion: premintVersion, + signature: signature, + quantityToMint: quantityToMint, + fixedPriceMinter: fixedPriceMinter, + mintArguments: mintArguments + }); } - // emit Preminted event - emit Preminted( - address(tokenContract), - newTokenId, - isNewContract, - premintConfig.uid, - contractConfig, - premintConfig.tokenConfig, - msg.sender, - quantityToMint - ); - } - - function _getOrCreateContract(ContractCreationConfig calldata contractConfig) private returns (IZoraCreator1155 tokenContract, bool isNewContract) { - address contractAddress = getContractAddress(contractConfig); - // first we see if the code is already deployed for the contract - isNewContract = contractAddress.code.length == 0; - - if (isNewContract) { - // if address doesn't exist for hash, create it - tokenContract = _createContract(contractConfig); - } else { - tokenContract = IZoraCreator1155(contractAddress); + { + emit PremintedV2({ + contractAddress: result.contractAddress, + tokenId: result.tokenId, + createdNewContract: result.createdNewContract, + uid: uid, + minter: msg.sender, + quantityMinted: quantityToMint + }); } } - function _createContract(ContractCreationConfig calldata contractConfig) private returns (IZoraCreator1155 tokenContract) { - // we need to build the setup actions, that must: - bytes[] memory setupActions = new bytes[](0); - - // create the contract via the factory. - address newContractAddresss = zora1155Factory.createContractDeterministic( - contractConfig.contractURI, - contractConfig.contractName, - // default royalty config is empty, since we set it on a token level - ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: 0, royaltyRecipient: address(0), royaltyMintSchedule: 0}), - payable(contractConfig.contractAdmin), - setupActions - ); - tokenContract = IZoraCreator1155(newContractAddresss); + /// Creates a new token on the given erc1155 contract on behalf of a creator, and mints x tokens to the executor of this transaction. + /// If the erc1155 contract hasn't been created yet, it will be created with the given config within this same transaction. + /// The creator must sign the intent to create the token, and must have mint new token permission on the erc1155 contract, + /// or match the contract admin on the contract creation config if the contract hasn't been created yet. + /// Contract address of the created contract is deterministically generated from the contract config and this contract's address. + /// @dev For use with v1 of premint config, PremintConfigV2, which supports setting `createReferral`. + /// @param contractConfig Parameters for creating a new contract, if one doesn't exist yet. Used to resolve the deterministic contract address. + /// @param premintConfig Parameters for creating the token, and minting the initial x tokens to the executor. + /// @param signature Signature of the creator of the token, which must match the signer of the premint config, or have permission to create new tokens on the erc1155 contract if it's already been created + /// @param quantityToMint How many tokens to mint to the mintRecipient + /// @param mintArguments mint arguments specifying the token mint recipient, mint comment, and mint referral + function premintV1( + ContractCreationConfig calldata contractConfig, + PremintConfig calldata premintConfig, + bytes calldata signature, + uint256 quantityToMint, + MintArguments memory mintArguments + ) public payable returns (PremintResult memory result) { + (bytes memory encodedPremint, bytes32 premintVersion) = PremintEncoding.encodePremintV1(premintConfig); + + result = ZoraCreator1155PremintExecutorImplLib.premint({ + zora1155Factory: zora1155Factory, + contractConfig: contractConfig, + encodedPremintConfig: encodedPremint, + premintVersion: premintVersion, + signature: signature, + quantityToMint: quantityToMint, + fixedPriceMinter: premintConfig.tokenConfig.fixedPriceMinter, + mintArguments: mintArguments + }); + + emit PremintedV2({ + contractAddress: result.contractAddress, + tokenId: result.tokenId, + createdNewContract: result.createdNewContract, + uid: premintConfig.uid, + minter: msg.sender, + quantityMinted: quantityToMint + }); } - /// Gets the deterministic contract address for the given contract creation config. + /// @notice Gets the deterministic contract address for the given contract creation config. /// Contract address is generated deterministically from a hash based on the contract uri, contract name, /// contract admin, and the msg.sender, which is this contract's address. function getContractAddress(ContractCreationConfig calldata contractConfig) public view returns (address) { - return - zora1155Factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); - } - - /// Recovers the signer of the given premint config created against the specified zora1155 contract address. - function recoverSigner(PremintConfig calldata premintConfig, address zor1155Address, bytes calldata signature) public view returns (address) { - return ZoraCreator1155Attribution.recoverSigner(premintConfig, signature, zor1155Address, block.chainid); + return ZoraCreator1155PremintExecutorImplLib.getContractAddress(zora1155Factory, contractConfig); } /// @notice Utility function to determine if a premint contract has been created for a uid of a premint, and if so, @@ -147,29 +153,65 @@ contract ZoraCreator1155PremintExecutorImpl is Ownable2StepUpgradeable, UUPSUpgr return (true, ERC1155DelegationStorageV1(contractAddress).delegatedTokenId(uid)); } - /// @notice Utility function to check if the signature is valid; i.e. the signature can be used to - /// mint a token with the given config. If the contract hasn't been created, then the signer - /// must match the contract admin on the premint config. If it has been created, the signer - /// must have permission to create new tokens on the erc1155 contract. + // @custom:deprecated use isValidSignatureV1 instead function isValidSignature( ContractCreationConfig calldata contractConfig, PremintConfig calldata premintConfig, bytes calldata signature ) public view returns (bool isValid, address contractAddress, address recoveredSigner) { contractAddress = getContractAddress(contractConfig); - recoveredSigner = recoverSigner(premintConfig, contractAddress, signature); - if (recoveredSigner == address(0)) { - return (false, contractAddress, address(0)); - } + (isValid, recoveredSigner) = isValidSignatureV1(contractConfig.contractAdmin, contractAddress, premintConfig, signature); + } - // if contract hasn't been created, signer must be the contract admin on the config - if (contractAddress.code.length == 0) { - isValid = recoveredSigner == contractConfig.contractAdmin; - } else { - // if contract has been created, signer must have mint new token permission - isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); - } + /// @notice Recovers the signer of a premint, and checks if the signer is authorized to sign the premint. + /// @dev for use with v1 of premint config, PremintConfig + /// @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 + /// @param premintConfig The premint config + /// @param signature The signature of the premint + /// @return isValid Whether the signature is valid + /// @return recoveredSigner The signer of the premint + function isValidSignatureV1( + address premintContractConfigContractAdmin, + address contractAddress, + PremintConfig calldata premintConfig, + bytes calldata signature + ) public view returns (bool isValid, address recoveredSigner) { + bytes32 hashedPremint = ZoraCreator1155Attribution.hashPremint(premintConfig); + + (isValid, recoveredSigner) = ZoraCreator1155Attribution.isValidSignature( + premintContractConfigContractAdmin, + contractAddress, + hashedPremint, + ZoraCreator1155Attribution.HASHED_VERSION_1, + signature + ); + } + + /// @notice Recovers the signer of a premint, and checks if the signer is authorized to sign the premint. + /// @dev for use with v2 of premint config, PremintConfig + /// @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 + /// @param premintConfig The premint config + /// @param signature The signature of the premint + /// @return isValid Whether the signature is valid + /// @return recoveredSigner The signer of the premint + function isValidSignatureV2( + address premintContractConfigContractAdmin, + address contractAddress, + PremintConfigV2 calldata premintConfig, + bytes calldata signature + ) public view returns (bool isValid, address recoveredSigner) { + bytes32 hashedPremint = ZoraCreator1155Attribution.hashPremint(premintConfig); + + (isValid, recoveredSigner) = ZoraCreator1155Attribution.isValidSignature( + premintContractConfigContractAdmin, + contractAddress, + hashedPremint, + ZoraCreator1155Attribution.HASHED_VERSION_2, + signature + ); } // upgrade related functionality @@ -194,4 +236,20 @@ contract ZoraCreator1155PremintExecutorImpl is Ownable2StepUpgradeable, UUPSUpgr function _equals(string memory a, string memory b) internal pure returns (bool) { return (keccak256(bytes(a)) == keccak256(bytes(b))); } + + // Deprecated functions: + + /// @custom:deprecated use premintV1 instead + function premint( + ContractCreationConfig calldata contractConfig, + PremintConfig calldata premintConfig, + bytes calldata signature, + uint256 quantityToMint, + string calldata mintComment + ) external payable returns (uint256 newTokenId) { + // encode legacy mint arguments to call current function: + MintArguments memory mintArguments = MintArguments({mintRecipient: msg.sender, mintComment: mintComment, mintReferral: address(0)}); + + return premintV1(contractConfig, premintConfig, signature, quantityToMint, mintArguments).tokenId; + } } diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol new file mode 100644 index 000000000..ce5fcef75 --- /dev/null +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ContractCreationConfig} from "./ZoraCreator1155Attribution.sol"; +import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; +import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol"; +import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; +import {IMinter1155} from "../interfaces/IMinter1155.sol"; +import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155PremintExecutor.sol"; + +library ZoraCreator1155PremintExecutorImplLib { + function getOrCreateContract( + IZoraCreator1155Factory zora1155Factory, + ContractCreationConfig calldata contractConfig + ) internal returns (IZoraCreator1155 tokenContract, bool isNewContract) { + address contractAddress = getContractAddress(zora1155Factory, contractConfig); + // first we see if the code is already deployed for the contract + isNewContract = contractAddress.code.length == 0; + + if (isNewContract) { + // if address doesn't exist for hash, create it + tokenContract = createContract(zora1155Factory, contractConfig); + } else { + tokenContract = IZoraCreator1155(contractAddress); + } + } + + function createContract( + IZoraCreator1155Factory zora1155Factory, + ContractCreationConfig calldata contractConfig + ) internal returns (IZoraCreator1155 tokenContract) { + // we need to build the setup actions, that must: + bytes[] memory setupActions = new bytes[](0); + + // create the contract via the factory. + address newContractAddresss = zora1155Factory.createContractDeterministic( + contractConfig.contractURI, + contractConfig.contractName, + // default royalty config is empty, since we set it on a token level + ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: 0, royaltyRecipient: address(0), royaltyMintSchedule: 0}), + payable(contractConfig.contractAdmin), + setupActions + ); + tokenContract = IZoraCreator1155(newContractAddresss); + } + + /// Gets the deterministic contract address for the given contract creation config. + /// Contract address is generated deterministically from a hash based on the contract uri, contract name, + /// contract admin, and the msg.sender, which is this contract's address. + function getContractAddress(IZoraCreator1155Factory zora1155Factory, ContractCreationConfig calldata contractConfig) internal view returns (address) { + return + zora1155Factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); + } + + function encodeMintArguments(address mintRecipient, string memory mintComment) internal pure returns (bytes memory) { + return abi.encode(mintRecipient, mintComment); + } + + function decodeMintArguments(bytes memory mintArguments) internal pure returns (address mintRecipient, string memory mintComment) { + return abi.decode(mintArguments, (address, string)); + } + + function premint( + IZoraCreator1155Factory zora1155Factory, + ContractCreationConfig calldata contractConfig, + bytes memory encodedPremintConfig, + bytes32 premintVersion, + bytes calldata signature, + uint256 quantityToMint, + address fixedPriceMinter, + IZoraCreator1155PremintExecutor.MintArguments memory mintArguments + ) internal returns (IZoraCreator1155PremintExecutor.PremintResult memory) { + // get or create the contract with the given params + // contract address is deterministic. + (IZoraCreator1155 tokenContract, bool isNewContract) = getOrCreateContract(zora1155Factory, contractConfig); + + // pass the signature and the premint config to the token contract to create the token. + // The token contract will verify the signature and that the signer has permission to create a new token. + // and then create and setup the token using the given token config. + uint256 newTokenId = tokenContract.delegateSetupNewToken(encodedPremintConfig, premintVersion, signature, msg.sender); + + _performMint(tokenContract, fixedPriceMinter, newTokenId, quantityToMint, mintArguments); + + return IZoraCreator1155PremintExecutor.PremintResult({contractAddress: address(tokenContract), tokenId: newTokenId, createdNewContract: isNewContract}); + } + + function _performMint( + IZoraCreator1155 tokenContract, + address fixedPriceMinter, + uint256 tokenId, + uint256 quantityToMint, + IZoraCreator1155PremintExecutor.MintArguments memory mintArguments + ) internal { + bytes memory mintSettings = abi.encode(mintArguments.mintRecipient, mintArguments.mintComment); + if (quantityToMint != 0) + // mint the number of specified tokens to the executor + tokenContract.mintWithRewards{value: msg.value}(IMinter1155(fixedPriceMinter), tokenId, quantityToMint, mintSettings, mintArguments.mintReferral); + } +} diff --git a/packages/1155-contracts/src/interfaces/IZoraCreator1155.sol b/packages/1155-contracts/src/interfaces/IZoraCreator1155.sol index cf00e8702..f3d2c3d99 100644 --- a/packages/1155-contracts/src/interfaces/IZoraCreator1155.sol +++ b/packages/1155-contracts/src/interfaces/IZoraCreator1155.sol @@ -10,7 +10,7 @@ import {IMinter1155} from "../interfaces/IMinter1155.sol"; import {IOwnable} from "../interfaces/IOwnable.sol"; import {IVersionedContract} from "./IVersionedContract.sol"; import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; -import {PremintConfig} from "../delegation/ZoraCreator1155Attribution.sol"; +import {PremintConfigV2} from "../delegation/ZoraCreator1155Attribution.sol"; /* @@ -71,6 +71,8 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IZoraCreator1155Errors, I /// @param minterArguments calldata for the minter contracts function mint(IMinter1155 minter, uint256 tokenId, uint256 quantity, bytes calldata minterArguments) external payable; + function mintWithRewards(IMinter1155 minter, uint256 tokenId, uint256 quantity, bytes calldata minterArguments, address mintReferral) external payable; + function adminMint(address recipient, uint256 tokenId, uint256 quantity, bytes memory data) external; function adminMintBatch(address recipient, uint256[] memory tokenIds, uint256[] memory quantities, bytes memory data) external; @@ -82,7 +84,12 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IZoraCreator1155Errors, I /// @param maxSupply maxSupply for the token, set to 0 for open edition function setupNewToken(string memory tokenURI, uint256 maxSupply) external returns (uint256 tokenId); - function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature, address sender) external returns (uint256 newTokenId); + function delegateSetupNewToken( + bytes memory premintConfigEncoded, + bytes32 premintVersion, + bytes calldata signature, + address sender + ) external returns (uint256 newTokenId); function updateTokenURI(uint256 tokenId, string memory _newURI) external; diff --git a/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol b/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol new file mode 100644 index 000000000..b66df3af3 --- /dev/null +++ b/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {PremintEncoding, ZoraCreator1155Attribution, ContractCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2} from "../delegation/ZoraCreator1155Attribution.sol"; +import {IZoraCreator1155Factory} from "./IZoraCreator1155Factory.sol"; + +// interface for legacy v1 of premint executor methods +// maintained in order to not break existing calls +// to legacy api when this api is upgraded +interface ILegacyZoraCreator1155PremintExecutor { + event Preminted( + address indexed contractAddress, + uint256 indexed tokenId, + bool indexed createdNewContract, + uint32 uid, + ContractCreationConfig contractConfig, + TokenCreationConfig tokenConfig, + address minter, + uint256 quantityMinted + ); + + function premint( + ContractCreationConfig calldata contractConfig, + PremintConfig calldata premintConfig, + bytes calldata signature, + uint256 quantityToMint, + string calldata mintComment + ) external payable returns (uint256 newTokenId); +} + +interface IZoraCreator1155PremintExecutorV1 { + function premintV1( + ContractCreationConfig calldata contractConfig, + PremintConfig calldata premintConfig, + bytes calldata signature, + uint256 quantityToMint, + IZoraCreator1155PremintExecutor.MintArguments calldata mintArguments + ) external payable returns (IZoraCreator1155PremintExecutor.PremintResult memory); + + function isValidSignatureV1( + address originalContractAdmin, + address contractAddress, + PremintConfig calldata premintConfig, + bytes calldata signature + ) external view returns (bool isValid, address recoveredSigner); +} + +interface IZoraCreator1155PremintExecutorV2 { + function premintV2( + ContractCreationConfig calldata contractConfig, + PremintConfigV2 calldata premintConfig, + bytes calldata signature, + uint256 quantityToMint, + IZoraCreator1155PremintExecutor.MintArguments calldata mintArguments + ) external payable returns (IZoraCreator1155PremintExecutor.PremintResult memory); + + function isValidSignatureV2( + address originalContractAdmin, + address contractAddress, + PremintConfigV2 calldata premintConfig, + bytes calldata signature + ) external view returns (bool isValid, address recoveredSigner); +} + +interface IZoraCreator1155PremintExecutor is IZoraCreator1155PremintExecutorV1, IZoraCreator1155PremintExecutorV2 { + struct MintArguments { + address mintRecipient; + string mintComment; + address mintReferral; + } + + struct PremintResult { + address contractAddress; + uint256 tokenId; + bool createdNewContract; + } + + event PremintedV2( + address indexed contractAddress, + uint256 indexed tokenId, + bool indexed createdNewContract, + uint32 uid, + address minter, + uint256 quantityMinted + ); + + function zora1155Factory() external view returns (IZoraCreator1155Factory); + + function getContractAddress(ContractCreationConfig calldata contractConfig) external view returns (address); +} diff --git a/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol b/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol index 221eff944..d09db27ae 100644 --- a/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol +++ b/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol @@ -32,7 +32,7 @@ import {TransferHelperUtils} from "../utils/TransferHelperUtils.sol"; import {ZoraCreator1155StorageV1} from "./ZoraCreator1155StorageV1.sol"; import {IZoraCreator1155Errors} from "../interfaces/IZoraCreator1155Errors.sol"; import {ERC1155DelegationStorageV1} from "../delegation/ERC1155DelegationStorageV1.sol"; -import {ZoraCreator1155Attribution, PremintTokenSetup, PremintConfig} from "../delegation/ZoraCreator1155Attribution.sol"; +import {ZoraCreator1155Attribution, DecodedCreatorAttribution, PremintTokenSetup, PremintConfig, PremintConfigV2, DelegatedTokenCreation, DelegatedTokenSetup} from "../delegation/ZoraCreator1155Attribution.sol"; /// Imagine. Mint. Enjoy. /// @title ZoraCreator1155Impl @@ -281,7 +281,7 @@ contract ZoraCreator1155Impl is return tokenId; } - function _setupNewTokenAndPermission(string calldata newURI, uint256 maxSupply, address user, uint256 permission) internal returns (uint256) { + function _setupNewTokenAndPermission(string memory newURI, uint256 maxSupply, address user, uint256 permission) internal returns (uint256) { uint256 tokenId = _setupNewToken(newURI, maxSupply); _addPermission(tokenId, user, permission); @@ -749,41 +749,48 @@ contract ZoraCreator1155Impl is /// Sets up a new token using a token configuration and a signature created for the token creation parameters. /// The signature must be created by an account with the PERMISSION_BIT_MINTER role on the contract. - /// @param premintConfig configuration of token to be created + /// @param premintConfig abi encoded configuration of token to be created + /// @param premintVersion version of the premint configuration /// @param signature EIP-712 Signature created on the premintConfig by an account with the PERMISSION_BIT_MINTER role on the contract. + /// @param sender original sender of the transaction, used to set the firstMinter function delegateSetupNewToken( - PremintConfig calldata premintConfig, + bytes memory premintConfig, + bytes32 premintVersion, bytes calldata signature, address sender - ) public nonReentrant returns (uint256 newTokenId) { + ) external nonReentrant returns (uint256 newTokenId) { + (DelegatedTokenSetup memory params, DecodedCreatorAttribution memory attribution, bytes[] memory tokenSetupActions) = DelegatedTokenCreation + .decodeAndRecoverDelegatedTokenSetup(premintConfig, premintVersion, signature, address(this), nextTokenId); + // if a token has already been created for a premint config with this uid: - if (delegatedTokenId[premintConfig.uid] != 0) { + if (delegatedTokenId[params.uid] != 0) { // return its token id - return delegatedTokenId[premintConfig.uid]; + return delegatedTokenId[params.uid]; } - validatePremint(premintConfig); - - bytes32 hashedPremintConfig = ZoraCreator1155Attribution.hashPremint(premintConfig); - - // recover the signer from the data - address creator = ZoraCreator1155Attribution.recoverSignerHashed(hashedPremintConfig, signature, address(this), block.chainid); - // this is what attributes this token to have been created by the original creator - emit CreatorAttribution(hashedPremintConfig, ZoraCreator1155Attribution.NAME, ZoraCreator1155Attribution.VERSION, creator, signature); + emit CreatorAttribution(attribution.structHash, attribution.domainName, attribution.version, attribution.creator, attribution.signature); + return _delegateSetupNewToken(params, attribution.creator, tokenSetupActions, sender); + } + + function _delegateSetupNewToken( + DelegatedTokenSetup memory params, + address creator, + bytes[] memory tokenSetupActions, + address sender + ) internal returns (uint256 newTokenId) { // require that the signer can create new tokens (is a valid creator) _requireAdminOrRole(creator, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); // create the new token; msg sender will have PERMISSION_BIT_ADMIN on the new token - newTokenId = _setupNewTokenAndPermission(premintConfig.tokenConfig.tokenURI, premintConfig.tokenConfig.maxSupply, msg.sender, PERMISSION_BIT_ADMIN); + newTokenId = _setupNewTokenAndPermission(params.tokenURI, params.maxSupply, msg.sender, PERMISSION_BIT_ADMIN); - delegatedTokenId[premintConfig.uid] = newTokenId; + _setCreateReferral(newTokenId, params.createReferral); - firstMinters[newTokenId] = sender; + delegatedTokenId[params.uid] = newTokenId; - // invoke setup actions for new token, to save contract size, first get them from an external lib - bytes[] memory tokenSetupActions = PremintTokenSetup.makeSetupNewTokenCalls(newTokenId, creator, premintConfig.tokenConfig); + firstMinters[newTokenId] = sender; // then invoke them, calling account should be original msg.sender, which has admin on the new token _multicallInternal(tokenSetupActions); @@ -794,16 +801,4 @@ contract ZoraCreator1155Impl is // grant the token creator as admin of the newly created token _addPermission(newTokenId, creator, PERMISSION_BIT_ADMIN); } - - function validatePremint(PremintConfig calldata premintConfig) private view { - if (premintConfig.tokenConfig.mintStart != 0 && premintConfig.tokenConfig.mintStart > block.timestamp) { - // if the mint start is in the future, then revert - revert MintNotYetStarted(); - } - if (premintConfig.deleted) { - // if the signature says to be deleted, then dont execute any further minting logic; - // return 0 - revert PremintDeleted(); - } - } } diff --git a/packages/1155-contracts/test/fixtures/Zora1155PremintFixtures.sol b/packages/1155-contracts/test/fixtures/Zora1155PremintFixtures.sol index 0e01c8fd2..99acb2d6c 100644 --- a/packages/1155-contracts/test/fixtures/Zora1155PremintFixtures.sol +++ b/packages/1155-contracts/test/fixtures/Zora1155PremintFixtures.sol @@ -5,36 +5,50 @@ import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {IZoraCreator1155Errors} from "../../src/interfaces/IZoraCreator1155Errors.sol"; import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; -import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; import {ProxyShim} from "../../src/utils/ProxyShim.sol"; -import {ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {ContractCreationConfig, TokenCreationConfigV2} from "../../src/delegation/ZoraCreator1155Attribution.sol"; library Zora1155PremintFixtures { function makeDefaultContractCreationConfig(address contractAdmin) internal pure returns (ContractCreationConfig memory) { return ContractCreationConfig({contractAdmin: contractAdmin, contractName: "blah", contractURI: "blah.contract"}); } - function defaultRoyaltyConfig(address royaltyRecipient) internal pure returns (ICreatorRoyaltiesControl.RoyaltyConfiguration memory) { - return ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: 10, royaltyRecipient: royaltyRecipient, royaltyMintSchedule: 0}); + function makeDefaultTokenCreationConfigV2(IMinter1155 fixedPriceMinter, address royaltyRecipient) internal pure returns (TokenCreationConfigV2 memory) { + return + TokenCreationConfigV2({ + tokenURI: "blah.token", + maxSupply: 20, + maxTokensPerAddress: 10, + pricePerToken: 0, + mintStart: 0, + mintDuration: 0, + fixedPriceMinter: address(fixedPriceMinter), + payoutRecipient: royaltyRecipient, + royaltyBPS: 0, + createReferral: address(0) + }); } - function makeDefaultTokenCreationConfig(IMinter1155 fixedPriceMinter, address royaltyRecipient) internal pure returns (TokenCreationConfig memory) { - ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = defaultRoyaltyConfig(royaltyRecipient); + function makeTokenCreationConfigV2WithCreateReferral( + IMinter1155 fixedPriceMinter, + address createReferral, + address royaltyRecipient + ) internal pure returns (TokenCreationConfigV2 memory) { return - TokenCreationConfig({ + TokenCreationConfigV2({ tokenURI: "blah.token", maxSupply: 10, maxTokensPerAddress: 5, pricePerToken: 0, mintStart: 0, mintDuration: 0, - royaltyMintSchedule: royaltyConfig.royaltyMintSchedule, - royaltyBPS: royaltyConfig.royaltyBPS, - royaltyRecipient: royaltyConfig.royaltyRecipient, - fixedPriceMinter: address(fixedPriceMinter) + fixedPriceMinter: address(fixedPriceMinter), + payoutRecipient: royaltyRecipient, + royaltyBPS: 10, + createReferral: createReferral }); } } diff --git a/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol b/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol index a19a92b17..55ba19ba2 100644 --- a/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol +++ b/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol @@ -11,8 +11,8 @@ import {ITransferHookReceiver} from "../../src/interfaces/ITransferHookReceiver. import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {UpgradeGate} from "../../src/upgrades/UpgradeGate.sol"; -import {PremintConfig, TokenCreationConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; -import {ZoraCreator1155Attribution} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {PremintConfigV2, TokenCreationConfigV2} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {ZoraCreator1155Attribution, PremintEncoding} from "../../src/delegation/ZoraCreator1155Attribution.sol"; import {IZoraCreator1155Errors} from "../../src/interfaces/IZoraCreator1155Errors.sol"; import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; @@ -38,7 +38,7 @@ contract MockTransferHookReceiver is ITransferHookReceiver { hasTransfer[id] = true; } - function supportsInterface(bytes4 testInterface) external pure override returns (bool) { + function supportsInterface(bytes4 testInterface) external view override returns (bool) { return testInterface == type(ITransferHookReceiver).interfaceId; } } @@ -1049,8 +1049,8 @@ contract ZoraCreator1155Test is Test { init(); - PremintConfig memory premintConfig = PremintConfig({ - tokenConfig: TokenCreationConfig({ + PremintConfigV2 memory premintConfig = PremintConfigV2({ + tokenConfig: TokenCreationConfigV2({ // Metadata URI for the created token tokenURI: "", // Max supply of the created token @@ -1063,14 +1063,12 @@ contract ZoraCreator1155Test is Test { mintStart: 0, // The duration of the mint, starting from the first mint of this token. 0 for infinite mintDuration: type(uint64).max - 1, - // RoyaltyMintSchedule for created tokens. Every nth token will go to the royalty recipient. - royaltyMintSchedule: 0, - // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + payoutRecipient: admin, royaltyBPS: 0, - // RoyaltyRecipient for created tokens. The address that will receive the royalty payments. - royaltyRecipient: address(0), // Fixed price minter address - fixedPriceMinter: address(fixedPriceMinter) + fixedPriceMinter: address(fixedPriceMinter), + // Default create referral + createReferral: address(0) }), // 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. @@ -1091,12 +1089,25 @@ contract ZoraCreator1155Test is Test { chainId := chainid() } - (uint8 v, bytes32 r, bytes32 s) = vm.sign(adminKey, ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, address(target), chainId)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + adminKey, + ZoraCreator1155Attribution.premintHashedTypeDataV4( + ZoraCreator1155Attribution.hashPremint(premintConfig), + address(target), + ZoraCreator1155Attribution.HASHED_VERSION_2, + chainId + ) + ); bytes memory signature = abi.encodePacked(r, s, v); vm.prank(collector); - uint256 tokenId = target.delegateSetupNewToken(premintConfig, signature, collectors[0]); + uint256 tokenId; + + { + (bytes memory premintConfigEncoded, bytes32 version) = PremintEncoding.encodePremintV2(premintConfig); + tokenId = target.delegateSetupNewToken(premintConfigEncoded, version, signature, collectors[0]); + } RewardsSettings memory settings = target.computeFreeMintRewards(quantity); diff --git a/packages/1155-contracts/test/premint/Zora1155PremintExecutorProxy.t.sol b/packages/1155-contracts/test/premint/Zora1155PremintExecutorProxy.t.sol index 962c115d8..2aac3c870 100644 --- a/packages/1155-contracts/test/premint/Zora1155PremintExecutorProxy.t.sol +++ b/packages/1155-contracts/test/premint/Zora1155PremintExecutorProxy.t.sol @@ -11,9 +11,11 @@ import {ZoraCreator1155PremintExecutorImpl} from "../../src/delegation/ZoraCreat import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; import {ProxyShim} from "../../src/utils/ProxyShim.sol"; -import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfigV2, PremintConfigV2} from "../../src/delegation/ZoraCreator1155Attribution.sol"; import {IOwnable2StepUpgradeable} from "../../src/utils/ownable/IOwnable2StepUpgradeable.sol"; import {IHasContractName} from "../../src/interfaces/IContractMetadata.sol"; +import {ZoraCreator1155PremintExecutorImplLib} from "../../src/delegation/ZoraCreator1155PremintExecutorImplLib.sol"; +import {IZoraCreator1155PremintExecutor} from "../../src/interfaces/IZoraCreator1155PremintExecutor.sol"; contract Zora1155PremintExecutorProxyTest is Test, IHasContractName { address internal owner; @@ -26,6 +28,8 @@ contract Zora1155PremintExecutorProxyTest is Test, IHasContractName { uint256 internal mintFeeAmount = 0.000777 ether; ZoraCreator1155PremintExecutorImpl preminterAtProxy; + IZoraCreator1155PremintExecutor.MintArguments defaultMintArguments; + function setUp() external { zora = makeAddr("zora"); owner = makeAddr("owner"); @@ -46,14 +50,16 @@ contract Zora1155PremintExecutorProxyTest is Test, IHasContractName { // access the executor implementation via the proxy, and initialize the admin preminterAtProxy = ZoraCreator1155PremintExecutorImpl(address(proxy)); preminterAtProxy.initialize(owner); + + defaultMintArguments = IZoraCreator1155PremintExecutor.MintArguments({mintRecipient: collector, mintComment: "blah", mintReferral: address(0)}); } function test_canInvokeImplementationMethods() external { // create premint config IMinter1155 fixedPriceMinter = ZoraCreator1155FactoryImpl(address(factoryProxy)).fixedPriceMinter(); - PremintConfig memory premintConfig = PremintConfig({ - tokenConfig: Zora1155PremintFixtures.makeDefaultTokenCreationConfig(fixedPriceMinter, creator), + PremintConfigV2 memory premintConfig = PremintConfigV2({ + tokenConfig: Zora1155PremintFixtures.makeDefaultTokenCreationConfigV2(fixedPriceMinter, creator), uid: 100, version: 0, deleted: false @@ -64,7 +70,13 @@ contract Zora1155PremintExecutorProxyTest is Test, IHasContractName { address deterministicAddress = preminterAtProxy.getContractAddress(contractConfig); // sign the premint - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, deterministicAddress, block.chainid); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4( + structHash, + deterministicAddress, + ZoraCreator1155Attribution.HASHED_VERSION_2, + block.chainid + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); @@ -75,7 +87,8 @@ contract Zora1155PremintExecutorProxyTest is Test, IHasContractName { // execute the premint vm.deal(collector, mintFeeAmount); vm.prank(collector); - uint256 tokenId = preminterAtProxy.premint{value: mintFeeAmount}(contractConfig, premintConfig, signature, quantityToMint, ""); + uint256 tokenId = preminterAtProxy + .premintV2{value: mintFeeAmount}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; assertEq(ZoraCreator1155Impl(deterministicAddress).balanceOf(collector, tokenId), 1); } diff --git a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol index f4874a694..5e0c4486a 100644 --- a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol +++ b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -15,10 +15,13 @@ import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/Z import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; import {ZoraCreator1155PremintExecutorImpl} from "../../src/delegation/ZoraCreator1155PremintExecutorImpl.sol"; -import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {IZoraCreator1155PremintExecutor} from "../../src/interfaces/IZoraCreator1155PremintExecutor.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, TokenCreationConfigV2, PremintConfigV2, PremintConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; import {UUPSUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; import {ProxyShim} from "../../src/utils/ProxyShim.sol"; import {IMinterErrors} from "../../src/interfaces/IMinterErrors.sol"; +import {ZoraCreator1155PremintExecutorImplLib} from "../../src/delegation/ZoraCreator1155PremintExecutorImplLib.sol"; +import {Zora1155PremintFixtures} from "../fixtures/Zora1155PremintFixtures.sol"; contract ZoraCreator1155PreminterTest is Test { uint256 internal constant CONTRACT_BASE_ID = 0; @@ -38,13 +41,13 @@ contract ZoraCreator1155PreminterTest is Test { address internal premintExecutor; address internal collector; - event Preminted( + IZoraCreator1155PremintExecutor.MintArguments defaultMintArguments; + + event PremintedV2( address indexed contractAddress, uint256 indexed tokenId, bool indexed createdNewContract, uint32 uid, - ContractCreationConfig contractConfig, - TokenCreationConfig tokenConfig, address minter, uint256 quantityMinted ); @@ -62,31 +65,103 @@ contract ZoraCreator1155PreminterTest is Test { factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); preminter = new ZoraCreator1155PremintExecutorImpl(factory); + + defaultMintArguments = IZoraCreator1155PremintExecutor.MintArguments({mintRecipient: premintExecutor, mintComment: "blah", mintReferral: address(0)}); } function makeDefaultContractCreationConfig() internal view returns (ContractCreationConfig memory) { return ContractCreationConfig({contractAdmin: creator, contractName: "blah", contractURI: "blah.contract"}); } - function makeDefaultTokenCreationConfig() internal view returns (TokenCreationConfig memory) { - IMinter1155 fixedPriceMinter = factory.defaultMinters()[0]; + function getFixedPriceMinter() internal view returns (IMinter1155) { + return factory.defaultMinters()[0]; + } + + function makeDefaultPremintConfig() internal view returns (PremintConfigV2 memory) { + return + PremintConfigV2({ + tokenConfig: Zora1155PremintFixtures.makeDefaultTokenCreationConfigV2(getFixedPriceMinter(), creator), + uid: 100, + version: 0, + deleted: false + }); + } + + function makePremintConfigWithCreateReferral(address createReferral) internal view returns (PremintConfigV2 memory) { return - TokenCreationConfig({ - tokenURI: "blah.token", - maxSupply: 10, - maxTokensPerAddress: 5, - pricePerToken: 0, - mintStart: 0, - mintDuration: 0, - royaltyMintSchedule: defaultRoyaltyConfig.royaltyMintSchedule, - royaltyBPS: defaultRoyaltyConfig.royaltyBPS, - royaltyRecipient: defaultRoyaltyConfig.royaltyRecipient, - fixedPriceMinter: address(fixedPriceMinter) + PremintConfigV2({ + tokenConfig: Zora1155PremintFixtures.makeTokenCreationConfigV2WithCreateReferral(getFixedPriceMinter(), createReferral, creator), + uid: 100, + version: 0, + deleted: false }); } - function makeDefaultPremintConfig() internal view returns (PremintConfig memory) { - return PremintConfig({tokenConfig: makeDefaultTokenCreationConfig(), uid: 100, version: 0, deleted: false}); + function makeDefaultV1PremintConfig() private view returns (PremintConfig memory) { + // make a v1 premint config + return + PremintConfig({ + tokenConfig: TokenCreationConfig({ + tokenURI: "blah.token", + maxSupply: 10, + maxTokensPerAddress: 5, + pricePerToken: 0, + mintStart: 0, + mintDuration: 0, + fixedPriceMinter: address(getFixedPriceMinter()), + royaltyRecipient: creator, + royaltyBPS: 10, + royaltyMintSchedule: 0 + }), + uid: 100, + version: 0, + deleted: false + }); + } + + function test_v1Signatures_workOnV2Contract() external { + // 1. Make contract creation params + + // configuration of contract to create + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultV1PremintConfig(); + + // how many tokens are minted to the executor + uint256 quantityToMint = 1; + uint256 chainId = block.chainid; + + // get contract hash, which is unique per contract creation config, and can be used + // retreive the address created for a contract + address contractAddress = preminter.getContractAddress(contractConfig); + + // 2. Call smart contract to get digest to sign for creation params. + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, ZoraCreator1155Attribution.HASHED_VERSION_1, chainId); + + // 3. Sign the digest + // create a signature with the digest for the params + bytes memory signature = _sign(creatorPrivateKey, digest); + + uint256 mintCost = mintFeeAmount * quantityToMint; + // this account will be used to execute the premint, and should result in a contract being created + vm.deal(premintExecutor, mintCost); + + // make sure sig is still valid using legacy method + (bool isValid, , ) = preminter.isValidSignature(contractConfig, premintConfig, signature); + assertTrue(isValid); + + // now check using new method + (isValid, ) = preminter.isValidSignatureV1(contractConfig.contractAdmin, contractAddress, premintConfig, signature); + assertTrue(isValid); + + // now call the premint function, using the same config that was used to generate the digest, and the signature + vm.prank(premintExecutor); + uint256 tokenId = preminter.premintV1{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; + + // get the contract address from the preminter based on the contract hash id. + IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); + // get the created contract, and make sure that tokens have been minted to the address + assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), quantityToMint); } function test_successfullyMintsTokens() external { @@ -94,19 +169,19 @@ contract ZoraCreator1155PreminterTest is Test { // configuration of contract to create ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; uint256 chainId = block.chainid; - string memory comment = "hi"; // get contract hash, which is unique per contract creation config, and can be used // retreive the address created for a contract address contractAddress = preminter.getContractAddress(contractConfig); // 2. Call smart contract to get digest to sign for creation params. - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, ZoraCreator1155Attribution.HASHED_VERSION_2, chainId); // 3. Sign the digest // create a signature with the digest for the params @@ -118,7 +193,7 @@ contract ZoraCreator1155PreminterTest is Test { // now call the premint function, using the same config that was used to generate the digest, and the signature vm.prank(premintExecutor); - uint256 tokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; // get the contract address from the preminter based on the contract hash id. IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); @@ -131,14 +206,15 @@ contract ZoraCreator1155PreminterTest is Test { premintConfig.tokenConfig.tokenURI = "blah2.token"; premintConfig.uid++; - digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, ZoraCreator1155Attribution.HASHED_VERSION_2, chainId); signature = _sign(creatorPrivateKey, digest); vm.deal(premintExecutor, mintCost); // premint with new token config and signature vm.prank(premintExecutor); - tokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + tokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; // a new token shoudl have been created, with x tokens minted to the executor, on the same contract address // as before since the contract config didnt change @@ -150,18 +226,18 @@ contract ZoraCreator1155PreminterTest is Test { // configuration of contract to create ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 chainId = block.chainid; - string memory comment = "hi"; // get contract hash, which is unique per contract creation config, and can be used // retreive the address created for a contract address contractAddress = preminter.getContractAddress(contractConfig); // 2. Call smart contract to get digest to sign for creation params. - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, ZoraCreator1155Attribution.HASHED_VERSION_2, chainId); // 3. Sign the digest // create a signature with the digest for the params @@ -171,7 +247,7 @@ contract ZoraCreator1155PreminterTest is Test { // now call the premint function, using the same config that was used to generate the digest, and the signature vm.prank(premintExecutor); - uint256 tokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premintV2(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; // get the contract address from the preminter based on the contract hash id. IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); @@ -187,17 +263,22 @@ contract ZoraCreator1155PreminterTest is Test { function test_premint_emitsCreatorAttribution_fromErc1155Contract() external { // build a premint ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // sign and execute premint uint256 chainId = block.chainid; address deterministicAddress = preminter.getContractAddress(contractConfig); - bytes32 structHash = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, deterministicAddress, chainId); - bytes memory signature = _sign(creatorPrivateKey, structHash); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4( + structHash, + deterministicAddress, + ZoraCreator1155Attribution.HASHED_VERSION_2, + chainId + ); + bytes memory signature = _sign(creatorPrivateKey, digest); uint256 quantityToMint = 4; - string memory comment = "hi"; uint256 mintCost = mintFeeAmount * quantityToMint; // this account will be used to execute the premint, and should result in a contract being created vm.deal(collector, mintCost); @@ -206,10 +287,10 @@ contract ZoraCreator1155PreminterTest is Test { // verify CreatorAttribution was emitted from the erc1155 contract vm.expectEmit(true, false, false, false, deterministicAddress); - emit CreatorAttribution(structHash, ZoraCreator1155Attribution.NAME, ZoraCreator1155Attribution.VERSION, creator, signature); + emit CreatorAttribution(structHash, ZoraCreator1155Attribution.NAME, ZoraCreator1155Attribution.VERSION_2, creator, signature); // create contract and token via premint - preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments); } function preminterCanMintTokens() internal { @@ -217,19 +298,19 @@ contract ZoraCreator1155PreminterTest is Test { // configuration of contract to create ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; uint256 chainId = block.chainid; - string memory comment = "hi"; console.log("loading preminter"); address contractAddress = preminter.getContractAddress(contractConfig); // 2. Call smart contract to get digest to sign for creation params. - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, ZoraCreator1155Attribution.HASHED_VERSION_2, chainId); // 3. Sign the digest // create a signature with the digest for the params @@ -241,7 +322,7 @@ contract ZoraCreator1155PreminterTest is Test { // now call the premint function, using the same config that was used to generate the digest, and the signature vm.deal(premintExecutor, mintCost); vm.prank(premintExecutor); - uint256 tokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; // get the contract address from the preminter based on the contract hash id. IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); @@ -258,7 +339,7 @@ contract ZoraCreator1155PreminterTest is Test { // configuration of contract to create ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 2; @@ -279,12 +360,18 @@ contract ZoraCreator1155PreminterTest is Test { uint256 nextTokenId; + uint256 beforeTokenBalance = created1155Contract.balanceOf(defaultMintArguments.mintRecipient, firstTokenId); + vm.startPrank(collector); // premint with new token config and signature, but same uid - it should mint tokens for the first token - nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + nextTokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; assertEq(nextTokenId, firstTokenId); - assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint); + assertEq( + created1155Contract.balanceOf(defaultMintArguments.mintRecipient, firstTokenId) - beforeTokenBalance, + quantityToMint, + "balance after first mint" + ); // change the version, it should still point to the first token premintConfig.version++; @@ -293,16 +380,20 @@ contract ZoraCreator1155PreminterTest is Test { vm.deal(collector, mintCost); // premint with new token config and signature - it should mint tokens for the first token - nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + nextTokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; vm.stopPrank(); assertEq(nextTokenId, firstTokenId); - assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint * 2); + assertEq( + created1155Contract.balanceOf(defaultMintArguments.mintRecipient, firstTokenId) - beforeTokenBalance, + quantityToMint * 2, + "balance after second mint" + ); } function testCreateTokenPerUid() public { ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); uint256 quantityToMint = 2; uint256 chainId = block.chainid; @@ -321,7 +412,7 @@ contract ZoraCreator1155PreminterTest is Test { vm.startPrank(collector); // premint with new token config and signature, but same uid - it should mint tokens for the first token - uint256 nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 nextTokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; assertEq(firstTokenId, 1); assertEq(nextTokenId, 2); @@ -329,12 +420,11 @@ contract ZoraCreator1155PreminterTest is Test { function test_deleted_preventsTokenFromBeingMinted() external { ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); premintConfig.deleted = true; uint chainId = block.chainid; uint256 quantityToMint = 2; - string memory comment = "I love it"; address contractAddress = preminter.getContractAddress(contractConfig); @@ -344,7 +434,7 @@ contract ZoraCreator1155PreminterTest is Test { // now call the premint function, using the same config that was used to generate the digest, and the signature vm.expectRevert(IZoraCreator1155Errors.PremintDeleted.selector); vm.prank(premintExecutor); - uint256 newTokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 newTokenId = preminter.premintV2(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; assertEq(newTokenId, 0, "tokenId"); @@ -354,7 +444,7 @@ contract ZoraCreator1155PreminterTest is Test { function test_emitsPremint_whenNewContract() external { ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); address contractAddress = preminter.getContractAddress(contractConfig); // how many tokens are minted to the executor @@ -366,8 +456,6 @@ contract ZoraCreator1155PreminterTest is Test { uint256 expectedTokenId = 1; - string memory comment = "I love it"; - uint256 mintCost = mintFeeAmount * quantityToMint; // this account will be used to execute the premint, and should result in a contract being created vm.deal(premintExecutor, mintCost); @@ -376,22 +464,13 @@ contract ZoraCreator1155PreminterTest is Test { bool createdNewContract = true; vm.expectEmit(true, true, true, true); - emit Preminted( - contractAddress, - expectedTokenId, - createdNewContract, - premintConfig.uid, - contractConfig, - premintConfig.tokenConfig, - premintExecutor, - quantityToMint - ); - preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + emit PremintedV2(contractAddress, expectedTokenId, createdNewContract, premintConfig.uid, premintExecutor, quantityToMint); + preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments); } function test_onlyOwner_hasAdminRights_onCreatedToken() public { ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -459,7 +538,7 @@ contract ZoraCreator1155PreminterTest is Test { } function test_premintStatus_getsStatus() external { - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -503,12 +582,11 @@ contract ZoraCreator1155PreminterTest is Test { vm.warp(currentTime); ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); premintConfig.tokenConfig.mintStart = startDate; uint256 quantityToMint = 4; uint256 chainId = block.chainid; - string memory comment = "I love it"; // get signature for the premint: bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, creatorPrivateKey, chainId); @@ -521,7 +599,7 @@ contract ZoraCreator1155PreminterTest is Test { vm.deal(premintExecutor, mintCost); vm.prank(premintExecutor); - preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments); } function test_premintCanOnlyBeExecutedUpToDurationFromFirstMint(uint8 startDate, uint8 duration, uint8 timeOfFirstMint, uint8 timeOfSecondMint) external { @@ -538,7 +616,7 @@ contract ZoraCreator1155PreminterTest is Test { // build a premint with a token that has the given start date and duration ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); address contractAddress = preminter.getContractAddress(contractConfig); premintConfig.tokenConfig.mintStart = startDate; @@ -558,7 +636,7 @@ contract ZoraCreator1155PreminterTest is Test { vm.startPrank(premintExecutor); vm.warp(timeOfFirstMint); - uint256 tokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, defaultMintArguments).tokenId; vm.warp(timeOfSecondMint); @@ -577,7 +655,7 @@ contract ZoraCreator1155PreminterTest is Test { function test_premintStatus_getsIfContractHasBeenCreatedAndTokenIdForPremint() external { // build a premint ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); // get premint status (bool contractCreated, uint256 tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid); @@ -604,14 +682,22 @@ contract ZoraCreator1155PreminterTest is Test { function test_premint_whenContractCreated_premintCanOnlyBeExecutedByPermissionBitMinter() external { // build a premint ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfigV2 memory premintConfig = makeDefaultPremintConfig(); + + address contractAddress = preminter.getContractAddress(contractConfig); // sign and execute premint - bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, creatorPrivateKey, block.chainid); + bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, block.chainid); - (bool isValidSignature, address contractAddress, ) = preminter.isValidSignature(contractConfig, premintConfig, signature); + (bool isValidSignature, address recoveredSigner) = preminter.isValidSignatureV2( + contractConfig.contractAdmin, + contractAddress, + premintConfig, + signature + ); - assertTrue(isValidSignature); + assertEq(creator, recoveredSigner, "recovered the wrong signer"); + assertTrue(isValidSignature, "signature should be valid"); _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, premintExecutor, 1, "hi"); @@ -620,16 +706,16 @@ contract ZoraCreator1155PreminterTest is Test { // have another creator sign a premint uint256 newCreatorPrivateKey = 0xA11CF; address newCreator = vm.addr(newCreatorPrivateKey); - PremintConfig memory premintConfig2 = premintConfig; + PremintConfigV2 memory premintConfig2 = premintConfig; premintConfig2.uid++; // have new creator sign a premint, isValidSignature should be false, and premint should revert bytes memory newCreatorSignature = _signPremint(contractAddress, premintConfig2, newCreatorPrivateKey, block.chainid); // it should not be considered a valid signature - (isValidSignature, , ) = preminter.isValidSignature(contractConfig, premintConfig2, newCreatorSignature); + (isValidSignature, ) = preminter.isValidSignatureV2(contractConfig.contractAdmin, contractAddress, premintConfig2, newCreatorSignature); - assertFalse(isValidSignature); + assertFalse(isValidSignature, "signature should not be valid"); uint256 quantityToMint = 1; uint256 mintCost = mintFeeAmount * quantityToMint; @@ -638,26 +724,41 @@ contract ZoraCreator1155PreminterTest is Test { // try to mint, it should revert vm.expectRevert(abi.encodeWithSelector(IZoraCreator1155Errors.UserMissingRoleForToken.selector, newCreator, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER)); vm.prank(premintExecutor); - preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "yo"); + preminter.premintV2{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, defaultMintArguments); // now grant the new creator permission to mint vm.prank(creator); IZoraCreator1155(contractAddress).addPermission(CONTRACT_BASE_ID, newCreator, PERMISSION_BIT_MINTER); // should now be considered a valid signature - (isValidSignature, , ) = preminter.isValidSignature(contractConfig, premintConfig2, newCreatorSignature); - assertTrue(isValidSignature); + (isValidSignature, ) = preminter.isValidSignatureV2(contractConfig.contractAdmin, contractAddress, premintConfig2, newCreatorSignature); + assertTrue(isValidSignature, "valid signature after granted permission"); vm.deal(premintExecutor, mintCost); // try to mint again, should not revert vm.prank(premintExecutor); - preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "yo"); + preminter.premintV2{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, defaultMintArguments); + } + + function testPremintWithCreateReferral() public { + address createReferral = makeAddr("createReferral"); + + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfigV2 memory premintConfig = makePremintConfigWithCreateReferral(createReferral); + + uint256 createdTokenId = _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, premintExecutor, 1, "hi"); + + ZoraCreator1155Impl createdContract = ZoraCreator1155Impl(preminter.getContractAddress(contractConfig)); + + address storedCreateReferral = createdContract.createReferrals(createdTokenId); + + assertEq(storedCreateReferral, createReferral); } function _signAndExecutePremint( ContractCreationConfig memory contractConfig, - PremintConfig memory premintConfig, + PremintConfigV2 memory premintConfig, uint256 privateKey, uint256 chainId, address executor, @@ -666,25 +767,31 @@ contract ZoraCreator1155PreminterTest is Test { ) private returns (uint256 newTokenId) { bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, privateKey, chainId); + IZoraCreator1155PremintExecutor.MintArguments memory mintArguments = IZoraCreator1155PremintExecutor.MintArguments({ + mintRecipient: executor, + mintComment: comment, + mintReferral: address(0) + }); + uint256 mintCost = mintFeeAmount * quantityToMint; vm.deal(executor, mintCost); // now call the premint function, using the same config that was used to generate the digest, and the signature vm.prank(executor); - newTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + newTokenId = preminter.premintV2{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, mintArguments).tokenId; } function _signPremint( address contractAddress, - PremintConfig memory premintConfig, + PremintConfigV2 memory premintConfig, uint256 privateKey, uint256 chainId - ) private pure returns (bytes memory) { - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + ) private pure returns (bytes memory signature) { + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, ZoraCreator1155Attribution.HASHED_VERSION_2, chainId); - // 3. Sign the digest // create a signature with the digest for the params - return _sign(privateKey, digest); + signature = _sign(privateKey, digest); } function _sign(uint256 privateKey, bytes32 digest) private pure returns (bytes memory) { diff --git a/packages/premint-sdk/src/preminter.test.ts b/packages/premint-sdk/src/preminter.test.ts index 5065842af..3b13fca39 100644 --- a/packages/premint-sdk/src/preminter.test.ts +++ b/packages/premint-sdk/src/preminter.test.ts @@ -69,7 +69,7 @@ const defaultContractConfig = ({ contractAdmin: Address; }): ContractCreationConfig => ({ contractAdmin, - contractURI: "ipfs://asdfasdfasdf", + contractURI: "ipfs://asdfasdfasdfgh", contractName: "My fun NFT", }); @@ -191,11 +191,11 @@ describe("ZoraCreator1155Preminter", () => { }); // recover and verify address is correct - const recoveredAddress = await publicClient.readContract({ + const [,,recoveredAddress] = await publicClient.readContract({ abi: preminterAbi, address: preminterAddress, - functionName: "recoverSigner", - args: [premintConfig, contractAddress, signedMessage], + functionName: "isValidSignature", + args: [contractConfig, premintConfig, signedMessage], }); expect(recoveredAddress).to.equal(creatorAccount); @@ -298,6 +298,7 @@ describe("ZoraCreator1155Preminter", () => { args: [contractAddress, premintConfig.uid], }); + expect(contractCreated).toBe(true); expect(tokenId).not.toBe(0n); // now use what was created, to get the balance from the created contract diff --git a/packages/protocol-deployments/package/chainConfigs.ts b/packages/protocol-deployments/package/chainConfigs.ts index f9ecddcef..afa4b70e2 100644 --- a/packages/protocol-deployments/package/chainConfigs.ts +++ b/packages/protocol-deployments/package/chainConfigs.ts @@ -52,4 +52,9 @@ export const chainConfigs = { MINT_FEE_RECIPIENT: "0xE84DBB2B25F761751231a9D0DAfbdD4dC3aa8252", PROTOCOL_REWARDS: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", }, + [999999999]: { + FACTORY_OWNER: "0xdae22ce69Afcb7f4bc37D32E267645722949DE0E", + MINT_FEE_RECIPIENT: "0xdae22ce69Afcb7f4bc37D32E267645722949DE0E", + PROTOCOL_REWARDS: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + }, }; diff --git a/packages/protocol-deployments/script/DeployProxiesToNewChain.s.sol b/packages/protocol-deployments/script/DeployProxiesToNewChain.s.sol index 58b46609b..6c4b842d6 100644 --- a/packages/protocol-deployments/script/DeployProxiesToNewChain.s.sol +++ b/packages/protocol-deployments/script/DeployProxiesToNewChain.s.sol @@ -31,7 +31,7 @@ contract DeployProxiesToNewChain is ZoraDeployerBase, DeploymentTestingUtils { console2.log("testing premint"); - signAndExecutePremint(deployment.preminterProxy); + signAndExecutePremint(deployment.preminterProxy, vm.envAddress("TEST_PREMINT_FUNDS_RECIPIENT")); vm.stopBroadcast(); diff --git a/packages/protocol-deployments/src/DeploymentTestingUtils.sol b/packages/protocol-deployments/src/DeploymentTestingUtils.sol index ab2e3719b..107bdd824 100644 --- a/packages/protocol-deployments/src/DeploymentTestingUtils.sol +++ b/packages/protocol-deployments/src/DeploymentTestingUtils.sol @@ -3,32 +3,33 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; import {IMinter1155} from "@zoralabs/zora-1155-contracts/src/interfaces/IMinter1155.sol"; +import {IZoraCreator1155PremintExecutor} from "@zoralabs/zora-1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol"; import {ZoraCreator1155PremintExecutorImpl} from "@zoralabs/zora-1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol"; import {ZoraCreator1155FactoryImpl} from "@zoralabs/zora-1155-contracts/src/factory/ZoraCreator1155FactoryImpl.sol"; -import {ZoraCreator1155Attribution, ContractCreationConfig, PremintConfig, TokenCreationConfig} from "@zoralabs/zora-1155-contracts/src/delegation/ZoraCreator1155Attribution.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig, PremintConfigV2, TokenCreationConfigV2} from "@zoralabs/zora-1155-contracts/src/delegation/ZoraCreator1155Attribution.sol"; import {ZoraCreator1155Impl} from "@zoralabs/zora-1155-contracts/src/nft/ZoraCreator1155Impl.sol"; contract DeploymentTestingUtils is Script { - function signAndExecutePremint(address premintExecutorProxyAddress) internal { + function signAndExecutePremint(address premintExecutorProxyAddress, address payoutRecipient) internal { console2.log("preminter proxy", premintExecutorProxyAddress); (address creator, uint256 creatorPrivateKey) = makeAddrAndKey("creator"); - ZoraCreator1155PremintExecutorImpl preminterAtProxy = ZoraCreator1155PremintExecutorImpl(premintExecutorProxyAddress); + IZoraCreator1155PremintExecutor preminterAtProxy = IZoraCreator1155PremintExecutor(premintExecutorProxyAddress); IMinter1155 fixedPriceMinter = ZoraCreator1155FactoryImpl(address(preminterAtProxy.zora1155Factory())).fixedPriceMinter(); - PremintConfig memory premintConfig = PremintConfig({ - tokenConfig: TokenCreationConfig({ + PremintConfigV2 memory premintConfig = PremintConfigV2({ + tokenConfig: TokenCreationConfigV2({ tokenURI: "blah.token", maxSupply: 10, maxTokensPerAddress: 5, pricePerToken: 0, mintStart: 0, mintDuration: 0, - royaltyMintSchedule: 0, royaltyBPS: 100, - royaltyRecipient: creator, - fixedPriceMinter: address(fixedPriceMinter) + payoutRecipient: payoutRecipient, + fixedPriceMinter: address(fixedPriceMinter), + createReferral: address(0) }), uid: 100, version: 0, @@ -39,18 +40,31 @@ contract DeploymentTestingUtils is Script { ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "blah", contractURI: "blah.contract"}); address deterministicAddress = preminterAtProxy.getContractAddress(contractConfig); - // sign the premint - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, deterministicAddress, block.chainid); + uint256 quantityToMint = 1; - (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPrivateKey, digest); + address mintRecipient = creator; - uint256 quantityToMint = 1; + IZoraCreator1155PremintExecutor.MintArguments memory mintArguments = IZoraCreator1155PremintExecutor.MintArguments({ + mintRecipient: mintRecipient, + mintComment: "", + mintReferral: address(0) + }); - bytes memory signature = abi.encodePacked(r, s, v); + bytes memory signature = signPremint(premintConfig, deterministicAddress, creatorPrivateKey); // execute the premint - uint256 tokenId = preminterAtProxy.premint{value: 0.000777 ether}(contractConfig, premintConfig, signature, quantityToMint, ""); + uint256 tokenId = preminterAtProxy.premintV2{value: 0.000777 ether}(contractConfig, premintConfig, signature, quantityToMint, mintArguments).tokenId; require(ZoraCreator1155Impl(deterministicAddress).delegatedTokenId(premintConfig.uid) == tokenId, "token id not created for uid"); } + + function signPremint(PremintConfigV2 memory premintConfig, address deterministicAddress, uint256 privateKey) private view returns (bytes memory signature) { + bytes32 signatureVersion = ZoraCreator1155Attribution.HASHED_VERSION_2; + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + // sign the premint + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, deterministicAddress, signatureVersion, block.chainid); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } } diff --git a/packages/protocol-deployments/test/ZoraCreator1155PremintExecutorForkTest.t.sol b/packages/protocol-deployments/test/ZoraCreator1155PremintExecutorForkTest.t.sol index 1f233d83a..3c48617e0 100644 --- a/packages/protocol-deployments/test/ZoraCreator1155PremintExecutorForkTest.t.sol +++ b/packages/protocol-deployments/test/ZoraCreator1155PremintExecutorForkTest.t.sol @@ -76,7 +76,12 @@ contract ZoraCreator1155PreminterForkTest is ForkDeploymentConfig, Test { uint256 privateKey, uint256 chainId ) private pure returns (bytes memory) { - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4( + ZoraCreator1155Attribution.hashPremint(premintConfig), + contractAddress, + ZoraCreator1155Attribution.HASHED_VERSION_1, + chainId + ); // 3. Sign the digest // create a signature with the digest for the params