From bc8f57cb97c0f27ddb49b167e5a730932841bf72 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 3 Aug 2023 16:08:19 -0700 Subject: [PATCH] wip on move premint to creator attribution style --- script/EstimatePreminterGas.s.sol | 90 ------ src/interfaces/IZoraCreator1155.sol | 3 + src/nft/ZoraCreator1155Impl.sol | 64 ++++ src/premint/EIP712UpgradeableWithChainId.sol | 106 ------- src/premint/ZoraCreator1155Delegation.sol | 238 +++++++++++++++ src/premint/ZoraCreator1155Preminter.sol | 296 ++----------------- src/utils/PublicMulticall.sol | 10 + test/premint/ZoraCreator1155Preminter.t.sol | 142 ++++----- 8 files changed, 414 insertions(+), 535 deletions(-) delete mode 100644 script/EstimatePreminterGas.s.sol delete mode 100644 src/premint/EIP712UpgradeableWithChainId.sol create mode 100644 src/premint/ZoraCreator1155Delegation.sol diff --git a/script/EstimatePreminterGas.s.sol b/script/EstimatePreminterGas.s.sol deleted file mode 100644 index e95737921..000000000 --- a/script/EstimatePreminterGas.s.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import "forge-std/Script.sol"; -import "forge-std/console2.sol"; - -import {ZoraDeployerBase} from "./ZoraDeployerBase.sol"; -import {ChainConfig, Deployment} from "../src/deployment/DeploymentConfig.sol"; - -import {ZoraCreator1155FactoryImpl} from "../src/factory/ZoraCreator1155FactoryImpl.sol"; -import {Zora1155Factory} from "../src/proxies/Zora1155Factory.sol"; -import {ZoraCreator1155Impl} from "../src/nft/ZoraCreator1155Impl.sol"; -import {ICreatorRoyaltiesControl} from "../src/interfaces/ICreatorRoyaltiesControl.sol"; -import {IZoraCreator1155Factory} from "../src/interfaces/IZoraCreator1155Factory.sol"; -import {IMinter1155} from "../src/interfaces/IMinter1155.sol"; -import {IZoraCreator1155} from "../src/interfaces/IZoraCreator1155.sol"; -import {ProxyShim} from "../src/utils/ProxyShim.sol"; -import {ZoraCreatorFixedPriceSaleStrategy} from "../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; -import {ZoraCreatorMerkleMinterStrategy} from "../src/minters/merkle/ZoraCreatorMerkleMinterStrategy.sol"; -import {ZoraCreatorRedeemMinterFactory} from "../src/minters/redeem/ZoraCreatorRedeemMinterFactory.sol"; -import {ZoraCreator1155Preminter} from "../src/premint/ZoraCreator1155Preminter.sol"; - -contract EstimatePreminterGas is ZoraDeployerBase { - function run() public { - Deployment memory deployment = getDeployment(); - - address deployer = vm.envAddress("DEPLOYER"); - - ZoraCreator1155FactoryImpl factory = ZoraCreator1155FactoryImpl(deployment.factoryProxy); - - console.log("deploying preminter contract"); - vm.startBroadcast(deployer); - - ZoraCreator1155Preminter preminter = new ZoraCreator1155Preminter(); - preminter.initialize(factory); - - vm.stopBroadcast(); - - // now generate a signature - - ZoraCreator1155Preminter.ContractCreationConfig memory contractConfig = ZoraCreator1155Preminter.ContractCreationConfig({ - contractAdmin: deployer, - contractName: "blah", - contractURI: "blah.contract" - }); - // configuration of token to create - ZoraCreator1155Preminter.TokenCreationConfig memory tokenConfig = ZoraCreator1155Preminter.TokenCreationConfig({ - tokenURI: "blah.token", - maxSupply: 10, - maxTokensPerAddress: 5, - pricePerToken: 0, - mintStart: 0, - mintDuration: 365 days, - royaltyBPS: 10, - royaltyRecipient: deployer, - royaltyMintSchedule: 20 - }); - // how many tokens are minted to the executor - uint256 quantityToMint = 1; - uint32 uid = 100; - uint32 version = 0; - ZoraCreator1155Preminter.PremintConfig memory premintConfig = ZoraCreator1155Preminter.PremintConfig({ - contractConfig: contractConfig, - tokenConfig: tokenConfig, - uid: uid, - deleted: false, - version: version - }); - - uint256 valueToSend = quantityToMint * ZoraCreator1155Impl(address(factory.implementation())).mintFee(); - - bytes32 digest = preminter.premintHashData(premintConfig, address(preminter), chainId()); - - uint256 privateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - - bytes memory signature = abi.encodePacked(r, s, v); - - string memory comment = "we love it!"; - - console.log("executing premint"); - // now do an on-chain premint - vm.startBroadcast(deployer); - - preminter.premint{value: valueToSend}(premintConfig, signature, quantityToMint, comment); - - vm.stopBroadcast(); - } -} diff --git a/src/interfaces/IZoraCreator1155.sol b/src/interfaces/IZoraCreator1155.sol index 7ffd1d490..fbeb2c9c0 100644 --- a/src/interfaces/IZoraCreator1155.sol +++ b/src/interfaces/IZoraCreator1155.sol @@ -9,6 +9,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 "../premint/ZoraCreator1155Delegation.sol"; /* @@ -104,6 +105,8 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IVersionedContract, IOwna /// @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) external returns (uint256 newTokenId); + function updateTokenURI(uint256 tokenId, string memory _newURI) external; function updateContractMetadata(string memory _newURI, string memory _newName) external; diff --git a/src/nft/ZoraCreator1155Impl.sol b/src/nft/ZoraCreator1155Impl.sol index b3c97a783..d233e5593 100644 --- a/src/nft/ZoraCreator1155Impl.sol +++ b/src/nft/ZoraCreator1155Impl.sol @@ -31,6 +31,7 @@ import {PublicMulticall} from "../utils/PublicMulticall.sol"; import {SharedBaseConstants} from "../shared/SharedBaseConstants.sol"; import {TransferHelperUtils} from "../utils/TransferHelperUtils.sol"; import {ZoraCreator1155StorageV1} from "./ZoraCreator1155StorageV1.sol"; +import {ZoraCreator1155Attribution, TokenSetup, PremintConfig} from "../premint/ZoraCreator1155Delegation.sol"; /// Imagine. Mint. Enjoy. /// @title ZoraCreator1155Impl @@ -722,4 +723,67 @@ contract ZoraCreator1155Impl is revert(); } } + + /* start eip712 functionality */ + bytes32 private constant _HASHED_NAME = keccak256(bytes("Preminter")); + bytes32 private constant _HASHED_VERSION = keccak256(bytes("1")); + bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + mapping(uint32 => bool) public uidUsed; + + // todo: move to its own contract + error PremintAlreadyExecuted(); + error MintNotYetStarted(); + error PremintDeleted(); + + event CreatorAttribution(bytes32 structHash, bytes32 domainName, bytes32 version, bytes signature); + + function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature) public returns (uint256 newTokenId) { + if (premintConfig.tokenConfig.mintStart != 0 && premintConfig.tokenConfig.mintStart > block.timestamp) { + // if the mint start is in the future, then revert + revert MintNotYetStarted(); + } + + // check that uid hasn't been used + if (uidUsed[premintConfig.uid]) { + revert PremintAlreadyExecuted(); + } else { + uidUsed[premintConfig.uid] = true; + } + + if (premintConfig.deleted) { + // if the signature says to be deleted, then dont execute any further minting logic; + // return 0 + revert PremintDeleted(); + } + + bytes32 hashedPremintConfig = ZoraCreator1155Attribution.hashPremint(premintConfig); + + // this is what attributes this token to have been created by the original creator + emit CreatorAttribution(hashedPremintConfig, ZoraCreator1155Attribution.HASHED_NAME, ZoraCreator1155Attribution.HASHED_VERSION, signature); + + // recover the signer from the data + address recoveredSigner = ZoraCreator1155Attribution.recoverSignerHashed(hashedPremintConfig, signature, address(this), block.chainid); + + // require that the signer can create new tokens (is a valid creator) + _requireAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); + + // temporarily grant msg sender admin permission to create new tokens + _addPermission(CONTRACT_BASE_ID, msg.sender, PERMISSION_BIT_MINTER); + + // get the new token id - it will fail if the recovered signer does not have PERMISSION_BIT_MINTER permission + newTokenId = setupNewToken(premintConfig.tokenConfig.tokenURI, premintConfig.tokenConfig.maxSupply); + // msg.sender should now have admin role on that token (lets make sure to remove it at the end of this call!!!) + + // invoke setup actions for new token, to save contract size, first get them from an external lib + bytes[] memory tokenSetupActions = TokenSetup.makeSetupNewTokenCalls(newTokenId, recoveredSigner, premintConfig.tokenConfig); + + // then invoke them, calling account should be original msg.sender + _multicallInternal(tokenSetupActions); + + // remove the token creator as admin of the newly created token: + _removePermission(newTokenId, msg.sender, PERMISSION_BIT_ADMIN); + // grant the token creator as admin of the newly created token + _addPermission(newTokenId, recoveredSigner, PERMISSION_BIT_ADMIN); + } } diff --git a/src/premint/EIP712UpgradeableWithChainId.sol b/src/premint/EIP712UpgradeableWithChainId.sol deleted file mode 100644 index 067a663f7..000000000 --- a/src/premint/EIP712UpgradeableWithChainId.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/EIP712.sol) - -pragma solidity ^0.8.17; - -import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; -import {Initializable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; - -/** - * @dev Same as OpenZeppelins' EIP712Upgradeable but allows the chain id to be passed as an argument, - * enabling a message to be signed to execute on on another chain - */ -abstract contract EIP712UpgradeableWithChainId is Initializable { - /* solhint-disable var-name-mixedcase */ - bytes32 private _HASHED_NAME; - bytes32 private _HASHED_VERSION; - bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - - /* solhint-enable var-name-mixedcase */ - - /** - * @dev Initializes the domain separator and parameter caches. - * - * The meaning of `name` and `version` is specified in - * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: - * - * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. - * - `version`: the current major version of the signing domain. - * - * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart - * contract upgrade]. - */ - function __EIP712_init(string memory name, string memory version) internal onlyInitializing { - __EIP712_init_unchained(name, version); - } - - function __EIP712_init_unchained(string memory name, string memory version) internal onlyInitializing { - bytes32 hashedName = keccak256(bytes(name)); - bytes32 hashedVersion = keccak256(bytes(version)); - _HASHED_NAME = hashedName; - _HASHED_VERSION = hashedVersion; - } - - /** - * @dev Returns the domain separator for the specified chain. - */ - function _domainSeparatorV4(uint256 chainId, address verifyingContract) internal view returns (bytes32) { - return _buildDomainSeparator(_TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), verifyingContract, chainId); - } - - function _buildDomainSeparator( - bytes32 typeHash, - bytes32 nameHash, - bytes32 versionHash, - address verifyingContract, - uint256 chainId - ) private pure returns (bytes32) { - return keccak256(abi.encode(typeHash, nameHash, versionHash, chainId, verifyingContract)); - } - - /** - * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this - * function returns the hash of the fully encoded EIP712 message for this domain. - * - * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: - * - * ```solidity - * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( - * keccak256("Mail(address to,string contents)"), - * mailTo, - * keccak256(bytes(mailContents)) - * ))); - * address signer = ECDSA.recover(digest, signature); - * ``` - */ - function _hashTypedDataV4(bytes32 structHash, address verifyingContract, uint256 chainId) internal view virtual returns (bytes32) { - return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract), structHash); - } - - /** - * @dev The hash of the name parameter for the EIP712 domain. - * - * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs - * are a concern. - */ - function _EIP712NameHash() internal view virtual returns (bytes32) { - return _HASHED_NAME; - } - - /** - * @dev The hash of the version parameter for the EIP712 domain. - * - * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs - * are a concern. - */ - function _EIP712VersionHash() internal view virtual returns (bytes32) { - return _HASHED_VERSION; - } - - /** - * @dev This empty reserved space is put in place to allow future versions to add new - * variables without shifting down storage in the inheritance chain. - * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - */ - uint256[50] private __gap; -} diff --git a/src/premint/ZoraCreator1155Delegation.sol b/src/premint/ZoraCreator1155Delegation.sol new file mode 100644 index 000000000..8180b980c --- /dev/null +++ b/src/premint/ZoraCreator1155Delegation.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {IMinter1155} from "../interfaces/IMinter1155.sol"; +import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.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"; + +struct ContractCreationConfig { + // Creator/admin of the created contract. Must match the account that signed the message + address contractAdmin; + // Metadata URI for the created contract + string contractURI; + // Name of the created contract + string contractName; +} + +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; + // RoyaltyMintSchedule for created tokens. Every nth token will go to the royalty recipient. + 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. + address royaltyRecipient; + // Fixed price minter address + address fixedPriceMinter; +} + +struct PremintConfig { + // The config for the contract to be created + ContractCreationConfig contractConfig; + // 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; +} + +/// @title 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. Incentivizes the third party to execute the transaction by offering +/// a reward in the form of minted tokens. +/// @author @oveddan +library ZoraCreator1155Attribution { + /* start eip712 functionality */ + bytes32 public constant HASHED_NAME = keccak256(bytes("Preminter")); + bytes32 public constant HASHED_VERSION = keccak256(bytes("1")); + bytes32 public constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /** + * @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 _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, + uint256 chainId + ) public pure returns (address signatory) { + // first validate the signature - the creator must match the signer of the message + return recoverSignerHashed(hashPremint(premintConfig), signature, erc1155Contract, chainId); + } + + function recoverSignerHashed( + bytes32 hashedPremintConfig, + bytes calldata signature, + address erc1155Contract, + uint256 chainId + ) public pure returns (address signatory) { + // first validate the signature - the creator must match the signer of the message + bytes32 digest = _hashTypedDataV4( + hashedPremintConfig, + // here we pass the current contract and chain id, ensuring that the message + // only works for the current chain and contract id + erc1155Contract, + chainId + ); + + signatory = ECDSAUpgradeable.recover(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. + /// @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) { + // 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); + } + + bytes32 constant CONTRACT_AND_TOKEN_DOMAIN = + keccak256( + "Premint(ContractCreationConfig contractConfig,TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)ContractCreationConfig(address contractAdmin,string contractURI,string contractName)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient)" + ); + + function hashPremint(PremintConfig calldata premintConfig) public pure returns (bytes32) { + return + keccak256( + abi.encode( + CONTRACT_AND_TOKEN_DOMAIN, + _hashContract(premintConfig.contractConfig), + _hashToken(premintConfig.tokenConfig), + premintConfig.uid, + premintConfig.version, + premintConfig.deleted + ) + ); + } + + bytes32 constant TOKEN_DOMAIN = + keccak256( + "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient)" + ); + + function _hashToken(TokenCreationConfig calldata tokenConfig) private pure returns (bytes32) { + return + keccak256( + abi.encode( + TOKEN_DOMAIN, + _stringHash(tokenConfig.tokenURI), + tokenConfig.maxSupply, + tokenConfig.maxTokensPerAddress, + tokenConfig.pricePerToken, + tokenConfig.mintStart, + tokenConfig.mintDuration, + tokenConfig.royaltyMintSchedule, + tokenConfig.royaltyBPS, + tokenConfig.royaltyRecipient + ) + ); + } + + bytes32 constant CONTRACT_DOMAIN = keccak256("ContractCreationConfig(address contractAdmin,string contractURI,string contractName)"); + + function _hashContract(ContractCreationConfig calldata contractConfig) private pure returns (bytes32) { + return + keccak256( + abi.encode(CONTRACT_DOMAIN, contractConfig.contractAdmin, _stringHash(contractConfig.contractURI), _stringHash(contractConfig.contractName)) + ); + } + + function _stringHash(string calldata value) private pure returns (bytes32) { + return keccak256(bytes(value)); + } +} + +library TokenSetup { + uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; + + function makeSetupNewTokenCalls( + uint256 newTokenId, + address contractAdmin, + TokenCreationConfig calldata tokenConfig + ) external 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 + // first, grant the fixed price sale strategy minting capabilities on the token + // tokenContract.addPermission(newTokenId, address(fixedPriceMinter), PERMISSION_BIT_MINTER); + calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, fixedPriceMinterAddress, PERMISSION_BIT_MINTER); + + // set the sales config on that token + calls[1] = abi.encodeWithSelector( + IZoraCreator1155.callSale.selector, + newTokenId, + IMinter1155(fixedPriceMinterAddress), + abi.encodeWithSelector( + ZoraCreatorFixedPriceSaleStrategy.setSale.selector, + newTokenId, + _buildNewSalesConfig(contractAdmin, tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration) + ) + ); + + // set the royalty config on that token: + calls[2] = abi.encodeWithSelector( + IZoraCreator1155.updateRoyaltiesForToken.selector, + newTokenId, + ICreatorRoyaltiesControl.RoyaltyConfiguration({ + royaltyBPS: tokenConfig.royaltyBPS, + royaltyRecipient: tokenConfig.royaltyRecipient, + royaltyMintSchedule: tokenConfig.royaltyMintSchedule + }) + ); + } + + function _buildNewSalesConfig( + address creator, + uint96 pricePerToken, + uint64 maxTokensPerAddress, + uint64 duration + ) private view returns (ZoraCreatorFixedPriceSaleStrategy.SalesConfig memory) { + uint64 saleStart = uint64(block.timestamp); + uint64 saleEnd = duration == 0 ? type(uint64).max : saleStart + duration; + + return + ZoraCreatorFixedPriceSaleStrategy.SalesConfig({ + pricePerToken: pricePerToken, + saleStart: saleStart, + saleEnd: saleEnd, + maxTokensPerAddress: maxTokensPerAddress, + fundsRecipient: creator + }); + } +} diff --git a/src/premint/ZoraCreator1155Preminter.sol b/src/premint/ZoraCreator1155Preminter.sol index 3f27818e6..b01ea652d 100644 --- a/src/premint/ZoraCreator1155Preminter.sol +++ b/src/premint/ZoraCreator1155Preminter.sol @@ -2,101 +2,34 @@ pragma solidity 0.8.17; import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; -import {EIP712UpgradeableWithChainId} from "./EIP712UpgradeableWithChainId.sol"; import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/security/ReentrancyGuardUpgradeable.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol"; import {SharedBaseConstants} from "../shared/SharedBaseConstants.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {IMinter1155} from "../interfaces/IMinter1155.sol"; +import {PremintConfig, ContractCreationConfig, TokenCreationConfig} from "./ZoraCreator1155Delegation.sol"; /// @title 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. Incentivizes the third party to execute the transaction by offering /// a reward in the form of minted tokens. /// @author @oveddan -contract ZoraCreator1155Preminter is EIP712UpgradeableWithChainId, Ownable2StepUpgradeable, ReentrancyGuardUpgradeable { +contract ZoraCreator1155Preminter { IZoraCreator1155Factory factory; - IMinter1155 fixedPriceMinter; /// @notice copied from SharedBaseConstants uint256 constant CONTRACT_BASE_ID = 0; - /// @notice This user role allows for any action to be performed - /// @dev copied from ZoraCreator1155Impl - uint256 constant PERMISSION_BIT_ADMIN = 2 ** 1; - /// @notice This user role allows for only mint actions to be performed. /// @dev copied from ZoraCreator1155Impl uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; - uint256 constant PERMISSION_BIT_SALES = 2 ** 3; - - /// @dev The resulting token id created for a permint. - /// determinstic contract address => token id => created token id - /// if token not created yet, result id will be 0 - mapping(address => mapping(uint32 => uint256)) public premintTokenId; - error PremintAlreadyExecuted(); error MintNotYetStarted(); error InvalidSignature(); - function initialize(IZoraCreator1155Factory _factory) public initializer { - __EIP712_init("Preminter", "0.0.1"); + // todo: make a constructor + function initialize(IZoraCreator1155Factory _factory) public { factory = _factory; - fixedPriceMinter = _factory.defaultMinters()[0]; - } - - struct ContractCreationConfig { - // Creator/admin of the created contract. Must match the account that signed the message - address contractAdmin; - // Metadata URI for the created contract - string contractURI; - // Name of the created contract - string contractName; - } - - 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; - // RoyaltyMintSchedule for created tokens. Every nth token will go to the royalty recipient. - 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. - address royaltyRecipient; - } - - struct PremintConfig { - // The config for the contract to be created - ContractCreationConfig contractConfig; - // 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 PremintStatus { - // If the signature has been executed - bool executed; - // If premint has been executed, the contract address - address contractAddress; - // If premint has been executed, the created token id - uint256 tokenId; } event Preminted( @@ -115,13 +48,10 @@ contract ZoraCreator1155Preminter is EIP712UpgradeableWithChainId, Ownable2StepU // this could include creating the contract. function premint( PremintConfig calldata premintConfig, - /// @notice Unique id of the token, used to ensure that multiple signatures can't be used to create the same intended token, in the case - /// that a signature is updated for a token, and the old signature is executed, two tokens for the same original intended token could be created. - /// Only one signature per token id, scoped to the contract hash can be executed. bytes calldata signature, uint256 quantityToMint, string calldata mintComment - ) public payable nonReentrant returns (address contractAddress, uint256 newTokenId) { + ) public payable returns (uint256 newTokenId) { // 1. Validate the signature. // 2. Create an erc1155 contract with the given name and uri and the creator as the admin/owner // 3. Allow this contract to create new new tokens on the contract @@ -130,40 +60,31 @@ contract ZoraCreator1155Preminter is EIP712UpgradeableWithChainId, Ownable2StepU // 6. Make the creator an admin of that token (and remove this contracts admin rights) // 7. Mint x tokens, as configured, to the executor of this transaction. - _validateSignature(premintConfig, signature); - - 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 (address(0), 0); - } - - ContractCreationConfig calldata contractConfig = premintConfig.contractConfig; - TokenCreationConfig calldata tokenConfig = premintConfig.tokenConfig; - // get or create the contract with the given params - (IZoraCreator1155 tokenContract, bool isNewContract) = _getOrCreateContract(contractConfig); - contractAddress = address(tokenContract); - - // make sure a token hasn't been minted for the premint token uid and contract address - if (premintTokenId[contractAddress][premintConfig.uid] != 0) { - revert PremintAlreadyExecuted(); - } + (IZoraCreator1155 tokenContract, bool isNewContract) = _getOrCreateContract(premintConfig.contractConfig); + address contractAddress = address(tokenContract); - // setup the new token, and its sales config - newTokenId = _setupNewTokenAndSale(tokenContract, contractConfig.contractAdmin, tokenConfig); - - premintTokenId[contractAddress][premintConfig.uid] = newTokenId; - - emit Preminted(contractAddress, newTokenId, isNewContract, premintConfig.uid, contractConfig, tokenConfig, msg.sender, quantityToMint); + newTokenId = tokenContract.delegateSetupNewToken(premintConfig, signature); // mint the initial x tokens for this new token id to the executor. address tokenRecipient = msg.sender; - tokenContract.mint{value: msg.value}(fixedPriceMinter, newTokenId, quantityToMint, abi.encode(tokenRecipient, mintComment)); + tokenContract.mint{value: msg.value}( + IMinter1155(premintConfig.tokenConfig.fixedPriceMinter), + newTokenId, + quantityToMint, + abi.encode(tokenRecipient, mintComment) + ); + + emit Preminted( + contractAddress, + newTokenId, + isNewContract, + premintConfig.uid, + premintConfig.contractConfig, + premintConfig.tokenConfig, + msg.sender, + quantityToMint + ); } function _getOrCreateContract(ContractCreationConfig calldata contractConfig) private returns (IZoraCreator1155 tokenContract, bool isNewContract) { @@ -181,10 +102,7 @@ contract ZoraCreator1155Preminter is EIP712UpgradeableWithChainId, Ownable2StepU function _createContract(ContractCreationConfig calldata contractConfig) private returns (IZoraCreator1155 tokenContract) { // we need to build the setup actions, that must: - // grant this contract ability to mint tokens - when a token is minted, this contract is - // granted admin rights on that token - bytes[] memory setupActions = new bytes[](1); - setupActions[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, CONTRACT_BASE_ID, address(this), PERMISSION_BIT_MINTER); + bytes[] memory setupActions = new bytes[](0); // create the contract via the factory. address newContractAddresss = factory.createContractDeterministic( @@ -198,169 +116,7 @@ contract ZoraCreator1155Preminter is EIP712UpgradeableWithChainId, Ownable2StepU tokenContract = IZoraCreator1155(newContractAddresss); } - function _setupNewTokenAndSale( - IZoraCreator1155 tokenContract, - address contractAdmin, - TokenCreationConfig calldata tokenConfig - ) private returns (uint256 newTokenId) { - // mint a new token, and get its token id - // this contract has admin rights on that token - - newTokenId = tokenContract.setupNewToken(tokenConfig.tokenURI, tokenConfig.maxSupply); - - // set up the sales strategy - // first, grant the fixed price sale strategy minting capabilities on the token - tokenContract.addPermission(newTokenId, address(fixedPriceMinter), PERMISSION_BIT_MINTER); - - // set the sales config on that token - tokenContract.callSale( - newTokenId, - fixedPriceMinter, - abi.encodeWithSelector( - ZoraCreatorFixedPriceSaleStrategy.setSale.selector, - newTokenId, - _buildNewSalesConfig(contractAdmin, tokenConfig.pricePerToken, tokenConfig.maxTokensPerAddress, tokenConfig.mintDuration) - ) - ); - - // set the royalty config on that token: - tokenContract.updateRoyaltiesForToken( - newTokenId, - ICreatorRoyaltiesControl.RoyaltyConfiguration({ - royaltyBPS: tokenConfig.royaltyBPS, - royaltyRecipient: tokenConfig.royaltyRecipient, - royaltyMintSchedule: tokenConfig.royaltyMintSchedule - }) - ); - - // remove this contract as admin of the newly created token: - tokenContract.removePermission(newTokenId, address(this), PERMISSION_BIT_ADMIN); - } - - function recoverSigner(PremintConfig calldata premintConfig, bytes calldata signature) public view returns (address signatory) { - // first validate the signature - the creator must match the signer of the message - bytes32 digest = premintHashData( - premintConfig, - // here we pass the current contract and chain id, ensuring that the message - // only works for the current chain and contract id - address(this), - block.chainid - ); - - signatory = ECDSAUpgradeable.recover(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. - /// @param premintConfig Premint config to hash - /// @param verifyingContract Contract address that signature is to be verified against - /// @param chainId Chain id that signature is to be verified on - function premintHashData(PremintConfig calldata premintConfig, address verifyingContract, uint256 chainId) public view returns (bytes32) { - bytes32 encoded = _hashPremintConfig(premintConfig); - - // build the struct hash to be signed - // here we pass the chain id, allowing the message to be signed for another chain - return _hashTypedDataV4(encoded, verifyingContract, chainId); - } - - bytes32 constant CONTRACT_AND_TOKEN_DOMAIN = - keccak256( - "Premint(ContractCreationConfig contractConfig,TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)ContractCreationConfig(address contractAdmin,string contractURI,string contractName)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient)" - ); - - function _hashPremintConfig(PremintConfig calldata premintConfig) private pure returns (bytes32) { - return - keccak256( - abi.encode( - CONTRACT_AND_TOKEN_DOMAIN, - _hashContract(premintConfig.contractConfig), - _hashToken(premintConfig.tokenConfig), - premintConfig.uid, - premintConfig.version, - premintConfig.deleted - ) - ); - } - - bytes32 constant TOKEN_DOMAIN = - keccak256( - "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient)" - ); - - function _hashToken(TokenCreationConfig calldata tokenConfig) private pure returns (bytes32) { - return - keccak256( - abi.encode( - TOKEN_DOMAIN, - _stringHash(tokenConfig.tokenURI), - tokenConfig.maxSupply, - tokenConfig.maxTokensPerAddress, - tokenConfig.pricePerToken, - tokenConfig.mintStart, - tokenConfig.mintDuration, - tokenConfig.royaltyMintSchedule, - tokenConfig.royaltyBPS, - tokenConfig.royaltyRecipient - ) - ); - } - - bytes32 constant CONTRACT_DOMAIN = keccak256("ContractCreationConfig(address contractAdmin,string contractURI,string contractName)"); - - function _hashContract(ContractCreationConfig calldata contractConfig) private pure returns (bytes32) { - return - keccak256( - abi.encode(CONTRACT_DOMAIN, contractConfig.contractAdmin, _stringHash(contractConfig.contractURI), _stringHash(contractConfig.contractName)) - ); - } - - function getPremintedTokenId(ContractCreationConfig calldata contractConfig, uint32 tokenUid) public view returns (uint256) { - address contractAddress = getContractAddress(contractConfig); - - return premintTokenId[contractAddress][tokenUid]; - } - - function premintHasBeenExecuted(ContractCreationConfig calldata contractConfig, uint32 tokenUid) public view returns (bool) { - return getPremintedTokenId(contractConfig, tokenUid) != 0; - } - - /// Validates that the signer of the signature matches the contract admin - /// Checks if the signature is used; if it is, reverts. - /// If it isn't mark that it has been used. - function _validateSignature(PremintConfig calldata premintConfig, bytes calldata signature) private view { - // first validate the signature - the creator must match the signer of the message - // contractAddress = getContractAddress(premintConfig.contractConfig); - address signatory = recoverSigner(premintConfig, signature); - - if (signatory != premintConfig.contractConfig.contractAdmin) { - revert InvalidSignature(); - } - } - function getContractAddress(ContractCreationConfig calldata contractConfig) public view returns (address) { return factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); } - - function _stringHash(string calldata value) private pure returns (bytes32) { - return keccak256(bytes(value)); - } - - function _buildNewSalesConfig( - address creator, - uint96 pricePerToken, - uint64 maxTokensPerAddress, - uint64 duration - ) private view returns (ZoraCreatorFixedPriceSaleStrategy.SalesConfig memory) { - uint64 saleStart = uint64(block.timestamp); - uint64 saleEnd = duration == 0 ? type(uint64).max : saleStart + duration; - - return - ZoraCreatorFixedPriceSaleStrategy.SalesConfig({ - pricePerToken: pricePerToken, - saleStart: saleStart, - saleEnd: saleEnd, - maxTokensPerAddress: maxTokensPerAddress, - fundsRecipient: creator - }); - } } diff --git a/src/utils/PublicMulticall.sol b/src/utils/PublicMulticall.sol index 8fea986fc..2c03e71a8 100644 --- a/src/utils/PublicMulticall.sol +++ b/src/utils/PublicMulticall.sol @@ -15,4 +15,14 @@ abstract contract PublicMulticall { results[i] = Address.functionDelegateCall(address(this), data[i]); } } + + /** + * @notice Receives and executes a batch of function calls on this contract. + */ + function _multicallInternal(bytes[] memory data) internal virtual returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + results[i] = Address.functionDelegateCall(address(this), data[i]); + } + } } diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index 1ab7068cb..4a2ffb4b0 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -17,6 +17,7 @@ import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; import {ZoraCreator1155Preminter} from "../../src/premint/ZoraCreator1155Preminter.sol"; import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Delegation.sol"; contract ZoraCreator1155PreminterTest is Test { ZoraCreator1155Preminter internal preminter; @@ -32,8 +33,8 @@ contract ZoraCreator1155PreminterTest is Test { uint256 indexed tokenId, bool indexed createdNewContract, uint32 uid, - ZoraCreator1155Preminter.ContractCreationConfig contractConfig, - ZoraCreator1155Preminter.TokenCreationConfig tokenConfig, + ContractCreationConfig contractConfig, + TokenCreationConfig tokenConfig, address minter, uint256 quantityMinted ); @@ -59,13 +60,14 @@ contract ZoraCreator1155PreminterTest is Test { creator = vm.addr(creatorPrivateKey); } - function makeDefaultContractCreationConfig() internal view returns (ZoraCreator1155Preminter.ContractCreationConfig memory) { - return ZoraCreator1155Preminter.ContractCreationConfig({contractAdmin: creator, contractName: "blah", contractURI: "blah.contract"}); + function makeDefaultContractCreationConfig() internal view returns (ContractCreationConfig memory) { + return ContractCreationConfig({contractAdmin: creator, contractName: "blah", contractURI: "blah.contract"}); } - function makeDefaultTokenCreationConfig() internal view returns (ZoraCreator1155Preminter.TokenCreationConfig memory) { + function makeDefaultTokenCreationConfig() internal view returns (TokenCreationConfig memory) { + IMinter1155 fixedPriceMinter = factory.defaultMinters()[0]; return - ZoraCreator1155Preminter.TokenCreationConfig({ + TokenCreationConfig({ tokenURI: "blah.token", maxSupply: 10, maxTokensPerAddress: 5, @@ -74,13 +76,14 @@ contract ZoraCreator1155PreminterTest is Test { mintDuration: 0, royaltyMintSchedule: defaultRoyaltyConfig.royaltyMintSchedule, royaltyBPS: defaultRoyaltyConfig.royaltyBPS, - royaltyRecipient: defaultRoyaltyConfig.royaltyRecipient + royaltyRecipient: defaultRoyaltyConfig.royaltyRecipient, + fixedPriceMinter: address(fixedPriceMinter) }); } - function makeDefaultPremintConfig() internal view returns (ZoraCreator1155Preminter.PremintConfig memory) { + function makeDefaultPremintConfig() internal view returns (PremintConfig memory) { return - ZoraCreator1155Preminter.PremintConfig({ + PremintConfig({ contractConfig: makeDefaultContractCreationConfig(), tokenConfig: makeDefaultTokenCreationConfig(), uid: 100, @@ -93,15 +96,19 @@ contract ZoraCreator1155PreminterTest is Test { // 1. Make contract creation params // configuration of contract to create - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig 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(premintConfig.contractConfig); + // 2. Call smart contract to get digest to sign for creation params. - bytes32 digest = preminter.premintHashData(premintConfig, address(preminter), chainId); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); // 3. Sign the digest // create a signature with the digest for the params @@ -109,14 +116,9 @@ contract ZoraCreator1155PreminterTest is Test { // this account will be used to execute the premint, and should result in a contract being created address premintExecutor = vm.addr(701); - // 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(premintConfig, signature, quantityToMint, comment); - - // 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(premintConfig.contractConfig); + uint256 tokenId = preminter.premint(premintConfig, signature, quantityToMint, comment); // get the contract address from the preminter based on the contract hash id. IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); @@ -129,12 +131,12 @@ contract ZoraCreator1155PreminterTest is Test { premintConfig.tokenConfig.tokenURI = "blah2.token"; premintConfig.uid++; - digest = preminter.premintHashData(premintConfig, address(preminter), chainId); + digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); signature = _sign(creatorPrivateKey, digest); // premint with new token config and signature vm.prank(premintExecutor); - (, tokenId) = preminter.premint(premintConfig, signature, quantityToMint, comment); + tokenId = preminter.premint(premintConfig, signature, quantityToMint, comment); // 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 @@ -145,7 +147,7 @@ contract ZoraCreator1155PreminterTest is Test { // 1. Make contract creation params // configuration of contract to create - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -161,7 +163,7 @@ contract ZoraCreator1155PreminterTest is Test { vm.startPrank(premintExecutor); // premint with new token config and signature - it should revert - vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Preminter.PremintAlreadyExecuted.selector)); + vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Impl.PremintAlreadyExecuted.selector)); preminter.premint(premintConfig, signature, quantityToMint, comment); // change the version, it should still revert @@ -169,7 +171,7 @@ contract ZoraCreator1155PreminterTest is Test { signature = _signPremint(premintConfig, creatorPrivateKey, chainId); // premint with new token config and signature - it should revert - vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Preminter.PremintAlreadyExecuted.selector)); + vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Impl.PremintAlreadyExecuted.selector)); preminter.premint(premintConfig, signature, quantityToMint, comment); // change the uid, it should not revert @@ -180,7 +182,7 @@ contract ZoraCreator1155PreminterTest is Test { } function test_deleted_preventsTokenFromBeingMinted() external { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); premintConfig.deleted = true; uint chainId = block.chainid; @@ -189,24 +191,21 @@ contract ZoraCreator1155PreminterTest is Test { string memory comment = "I love it"; // 2. Call smart contract to get digest to sign for creation params. - (address contractAddress, uint256 tokenId) = _signAndExecutePremint( - premintConfig, - creatorPrivateKey, - chainId, - premintExecutor, - quantityToMint, - comment - ); + bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + + // now call the premint function, using the same config that was used to generate the digest, and the signature + vm.expectRevert(ZoraCreator1155Impl.PremintDeleted.selector); + vm.prank(premintExecutor); + uint256 newTokenId = preminter.premint(premintConfig, signature, quantityToMint, comment); - assertEq(contractAddress, address(0)); - assertEq(tokenId, 0); + assertEq(newTokenId, 0, "tokenId"); // make sure no contract was created - assertEq(preminter.getContractAddress(premintConfig.contractConfig).code.length, 0); + assertEq(preminter.getContractAddress(premintConfig.contractConfig).code.length, 0, "contract has been deployed"); } function test_emitsPremint_whenNewContract() external { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -224,11 +223,10 @@ contract ZoraCreator1155PreminterTest is Test { // we need the contract address to assert the emitted event, so lets premint, get the contract address, rollback, and premint again uint256 snapshot = vm.snapshot(); - (address contractAddress, uint256 tokenId) = preminter.premint(premintConfig, signature, quantityToMint, comment); + address contractAddress = preminter.getContractAddress(premintConfig.contractConfig); + uint256 tokenId = preminter.premint(premintConfig, signature, quantityToMint, comment); vm.revertTo(snapshot); - // vm.roll(currentBlock + 1); - // now call the premint function, using the same config that was used to generate the digest, and the signature bool createdNewContract = true; vm.expectEmit(true, true, true, true); @@ -246,7 +244,7 @@ contract ZoraCreator1155PreminterTest is Test { } function test_onlyOwner_hasAdminRights_onCreatedToken() public { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -320,7 +318,7 @@ contract ZoraCreator1155PreminterTest is Test { } function test_premintStatus_getsStatus() external { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -332,14 +330,14 @@ contract ZoraCreator1155PreminterTest is Test { uint32 firstUid = premintConfig.uid; uint32 secondUid = firstUid + 1; - ZoraCreator1155Preminter.ContractCreationConfig memory firstContractConfig = premintConfig.contractConfig; - ZoraCreator1155Preminter.ContractCreationConfig memory secondContractConfig = ZoraCreator1155Preminter.ContractCreationConfig( + ContractCreationConfig memory firstContractConfig = premintConfig.contractConfig; + ContractCreationConfig memory secondContractConfig = ContractCreationConfig( firstContractConfig.contractAdmin, firstContractConfig.contractURI, string.concat(firstContractConfig.contractName, "4") ); - (address resultContractAddress, uint256 newTokenId) = _signAndExecutePremint( + (address firstResultContractAddress, uint256 firstResultTokenId) = _signAndExecutePremint( premintConfig, creatorPrivateKey, chainId, @@ -347,27 +345,34 @@ contract ZoraCreator1155PreminterTest is Test { quantityToMint, comment ); - address contractAddress = preminter.getContractAddress(firstContractConfig); - uint256 tokenId = preminter.getPremintedTokenId(firstContractConfig, firstUid); - assertEq(contractAddress, resultContractAddress); - assertEq(tokenId, newTokenId); + assertEq(IZoraCreator1155(firstResultContractAddress).balanceOf(premintExecutor, firstResultTokenId), quantityToMint); premintConfig.uid = secondUid; - (resultContractAddress, newTokenId) = _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - tokenId = preminter.getPremintedTokenId(firstContractConfig, secondUid); + (address secondResultContractAddress, uint256 secondResultTokenId) = _signAndExecutePremint( + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); - assertEq(contractAddress, resultContractAddress); - assertEq(tokenId, newTokenId); + assertEq(firstResultContractAddress, secondResultContractAddress); + assertEq(IZoraCreator1155(firstResultContractAddress).balanceOf(premintExecutor, secondResultTokenId), quantityToMint); premintConfig.contractConfig = secondContractConfig; + (address thirdResultContractAddress, uint256 thirdResultTokenId) = _signAndExecutePremint( + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); - (resultContractAddress, newTokenId) = _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - contractAddress = preminter.getContractAddress(secondContractConfig); - tokenId = preminter.getPremintedTokenId(secondContractConfig, secondUid); - - assertEq(contractAddress, resultContractAddress); - assertEq(tokenId, newTokenId); + assertFalse(firstResultContractAddress == thirdResultContractAddress); + assertEq(IZoraCreator1155(thirdResultContractAddress).balanceOf(premintExecutor, thirdResultTokenId), quantityToMint); } function test_premintCanOnlyBeExecutedAfterStartDate(uint8 startDate, uint8 currentTime) external { @@ -380,7 +385,7 @@ contract ZoraCreator1155PreminterTest is Test { } vm.warp(currentTime); - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); premintConfig.tokenConfig.mintStart = startDate; uint256 quantityToMint = 4; @@ -411,7 +416,7 @@ contract ZoraCreator1155PreminterTest is Test { } // build a premint with a token that has the given start date and duration - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); premintConfig.tokenConfig.mintStart = startDate; premintConfig.tokenConfig.mintDuration = duration; @@ -427,39 +432,38 @@ contract ZoraCreator1155PreminterTest is Test { vm.startPrank(premintExecutor); vm.warp(timeOfFirstMint); - (address contractAddress, uint256 tokenId) = preminter.premint(premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premint(premintConfig, signature, quantityToMint, comment); vm.warp(timeOfSecondMint); + address contractAddress = preminter.getContractAddress(premintConfig.contractConfig); // execute mint directly on the contract - and check make sure it reverts if minted after sale start IMinter1155 fixedPriceMinter = factory.defaultMinters()[0]; if (shouldRevert) { vm.expectRevert(ZoraCreatorFixedPriceSaleStrategy.SaleEnded.selector); } + IZoraCreator1155(contractAddress).mint(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment)); } function _signAndExecutePremint( - ZoraCreator1155Preminter.PremintConfig memory premintConfig, + PremintConfig memory premintConfig, uint256 privateKey, uint256 chainId, address executor, uint256 quantityToMint, string memory comment - ) private returns (address, uint256) { + ) private returns (address contractAddress, uint256 newTokenId) { bytes memory signature = _signPremint(premintConfig, privateKey, chainId); // now call the premint function, using the same config that was used to generate the digest, and the signature + contractAddress = preminter.getContractAddress(premintConfig.contractConfig); vm.prank(executor); - return preminter.premint(premintConfig, signature, quantityToMint, comment); + newTokenId = preminter.premint(premintConfig, signature, quantityToMint, comment); } - function _signPremint( - ZoraCreator1155Preminter.PremintConfig memory premintConfig, - uint256 privateKey, - uint256 chainId - ) private view returns (bytes memory) { - bytes32 digest = preminter.premintHashData(premintConfig, address(preminter), chainId); + function _signPremint(PremintConfig memory premintConfig, uint256 privateKey, uint256 chainId) private view returns (bytes memory) { + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, preminter.getContractAddress(premintConfig.contractConfig), chainId); // 3. Sign the digest // create a signature with the digest for the params