From 05566bc72fe2bbdd07f89d8403e5a71fc17ef2da Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 22 Aug 2023 10:37:30 -0700 Subject: [PATCH] Creator attribution - reduce optimizer runs to get contracts to build (#144) * reduce optimizer runs to get contracts to build * Premint V2 - Creator Attribution (#128) * wip on move premint to creator attribution style * updated readme to reflect new contracts * Revert "undo changes to js sdk" This reverts commit 4deabf56e8fe4ee08fd1d395097ca083df06f5b2. * better comments * Added methods to get status of creator attribution, and validate signatures, useful for the backend * better comments * fixed back fork test * slightly more comments --- .changeset/twelve-comics-sniff.md | 5 + .env.anvil | 2 +- foundry.toml | 2 +- package/preminter.test.ts | 216 ++++++---- package/preminter.ts | 35 +- script/DeployPreminter.s.sol | 46 ++- script/EstimatePreminterGas.s.sol | 90 ----- src/interfaces/IZoraCreator1155.sol | 5 + src/nft/ZoraCreator1155Impl.sol | 43 ++ src/premint/EIP712UpgradeableWithChainId.sol | 106 ----- src/premint/ZoraCreator1155Attribution.sol | 237 +++++++++++ .../ZoraCreator1155PremintExecutor.sol | 159 ++++++++ src/premint/ZoraCreator1155Preminter.sol | 366 ----------------- src/utils/PublicMulticall.sol | 10 + test/premint/ZoraCreator1155Preminter.t.sol | 377 +++++++++++++----- uml/gasslessCreate-collecting-sequence.puml | 7 +- uml/gasslessCreate-creation-activity.puml | 8 +- uml/gasslessCreate-creation-sequence.puml | 24 +- .../gasslessCreate-collecting-sequence.svg | 2 +- .../gasslessCreate-creation-activity.svg | 2 +- .../gasslessCreate-creation-sequence.svg | 2 +- wagmi.config.ts | 10 +- 22 files changed, 954 insertions(+), 800 deletions(-) create mode 100644 .changeset/twelve-comics-sniff.md delete mode 100644 script/EstimatePreminterGas.s.sol delete mode 100644 src/premint/EIP712UpgradeableWithChainId.sol create mode 100644 src/premint/ZoraCreator1155Attribution.sol create mode 100644 src/premint/ZoraCreator1155PremintExecutor.sol delete mode 100644 src/premint/ZoraCreator1155Preminter.sol diff --git a/.changeset/twelve-comics-sniff.md b/.changeset/twelve-comics-sniff.md new file mode 100644 index 000000000..344d9184c --- /dev/null +++ b/.changeset/twelve-comics-sniff.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/zora-1155-contracts": minor +--- + +Premint with Delegated Minting diff --git a/.env.anvil b/.env.anvil index c66032f53..e1129784f 100644 --- a/.env.anvil +++ b/.env.anvil @@ -1,2 +1,2 @@ FORK_RPC_URL="https://testnet.rpc.zora.co/" -FORK_BLOCK_NUMBER=700700 \ No newline at end of file +FORK_BLOCK_NUMBER=906028 \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 83304a006..2d32d4992 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ fs_permissions = [{access = "read", path = "./addresses"}, {access = "read", path = "./chainConfigs"}, {access = "read", path = "./package.json"}] libs = ['_imagine', 'node_modules', 'script'] optimizer = true -optimizer_runs = 3000 +optimizer_runs = 500 out = 'out' solc_version = '0.8.17' src = 'src' diff --git a/package/preminter.test.ts b/package/preminter.test.ts index 4212de0aa..9dd383b78 100644 --- a/package/preminter.test.ts +++ b/package/preminter.test.ts @@ -4,14 +4,18 @@ import { createWalletClient, createPublicClient, } from "viem"; -import { foundry } from "viem/chains"; +import { foundry, zoraTestnet } from "viem/chains"; import { describe, it, beforeEach, expect } from "vitest"; import { parseEther } from "viem"; import { - zoraCreator1155PreminterABI as preminterAbi, + zoraCreator1155PremintExecutorABI as preminterAbi, + zoraCreator1155PremintExecutorAddress, zoraCreator1155ImplABI, + zoraCreator1155FactoryImplAddress, + zoraCreator1155FactoryImplConfig, } from "./wagmiGenerated"; -import preminter from "../out/ZoraCreator1155Preminter.sol/ZoraCreator1155Preminter.json"; +import ZoraCreator1155Attribution from "../out/ZoraCreator1155Attribution.sol/ZoraCreator1155Attribution.json"; +import zoraCreator1155PremintExecutor from "../out/ZoraCreator1155PremintExecutor.sol/ZoraCreator1155PremintExecutor.json"; import zoraCreator1155Impl from "../out/ZoraCreator1155Impl.sol/ZoraCreator1155Impl.json"; import zoraCreator1155FactoryImpl from "../out/ZoraCreator1155FactoryImpl.sol/ZoraCreator1155FactoryImpl.json"; import zoraCreatorFixedPriceSaleStrategy from "../out/ZoraCreatorFixedPriceSaleStrategy.sol/ZoraCreatorFixedPriceSaleStrategy.json"; @@ -58,8 +62,10 @@ const [ type TestContext = { preminterAddress: `0x${string}`; + forkedChainId: keyof typeof zoraCreator1155FactoryImplAddress; anvilChainId: number; zoraMintFee: bigint; + fixedPriceMinterAddress: Address; }; const deployContractAndGetAddress = async ( @@ -74,6 +80,7 @@ const deployContractAndGetAddress = async ( }; export const deployFactoryProxy = async () => { + console.log("deploying protocol rewards"); const protocolRewardsAddress = await deployContractAndGetAddress({ abi: protocolRewards.abi, bytecode: protocolRewards.bytecode.object as `0x${string}`, @@ -81,13 +88,16 @@ export const deployFactoryProxy = async () => { args: [], }); - // const mockUpgradeGateAddress = await deployContractAndGetAddress({ - // abi: mockUpgradeGate.abi, - // bytecode: mockUpgradeGate.bytecode.object as `0x${string}`, - // account: deployerAccount, - // args: [] - // }); + console.log("deploying attribution lib"); + const attributionAddress = await deployContractAndGetAddress({ + abi: ZoraCreator1155Attribution.abi, + bytecode: ZoraCreator1155Attribution.bytecode.object as `0x${string}`, + account: deployerAccount, + }); + + console.log("attribution address is ", attributionAddress); + console.log("deploying 1155"); const zora1155Address = await deployContractAndGetAddress({ abi: zoraCreator1155Impl.abi, bytecode: zoraCreator1155Impl.bytecode.object as `0x${string}`, @@ -95,6 +105,7 @@ export const deployFactoryProxy = async () => { args: [0n, mintFeeRecipientAccount, zeroAddress, protocolRewardsAddress], }); + console.log("deploying fixed priced minter"); const fixedPriceMinterAddress = await deployContractAndGetAddress({ abi: zoraCreatorFixedPriceSaleStrategy.abi, bytecode: zoraCreatorFixedPriceSaleStrategy.bytecode @@ -102,6 +113,7 @@ export const deployFactoryProxy = async () => { account: deployerAccount, }); + console.log("deploying factory impl"); const factoryImplAddress = await deployContractAndGetAddress({ abi: zoraCreator1155FactoryImpl.abi, bytecode: zoraCreator1155FactoryImpl.bytecode.object as `0x${string}`, @@ -111,16 +123,15 @@ export const deployFactoryProxy = async () => { const factoryProxyAddress = factoryImplAddress!; - return { factoryProxyAddress, zora1155Address: zora1155Address! }; + return { factoryProxyAddress, zora1155Address, fixedPriceMinterAddress }; }; -export const deployPreminterContract = async () => { - const { factoryProxyAddress, zora1155Address } = await deployFactoryProxy(); - +export const deployPreminterContract = async (factoryProxyAddress: Address) => { const deployPreminterHash = await walletClient.deployContract({ - abi: preminter.abi, - bytecode: preminter.bytecode.object as `0x${string}`, + abi: zoraCreator1155PremintExecutor.abi, + bytecode: zoraCreator1155PremintExecutor.bytecode.object as `0x${string}`, account: deployerAccount, + args: [factoryProxyAddress], }); const receipt = await publicClient.waitForTransactionReceipt({ @@ -129,47 +140,43 @@ export const deployPreminterContract = async () => { const preminterAddress = receipt.contractAddress!; - const initializeHash = await walletClient.writeContract({ - abi: preminterAbi, - address: preminterAddress, - functionName: "initialize", - account: deployerAccount, - args: [factoryProxyAddress], - }); - - await publicClient.waitForTransactionReceipt({ hash: initializeHash }); - - return { preminterAddress, factoryProxyAddress, zora1155Address }; + return { preminterAddress, factoryProxyAddress }; }; // create token and contract creation config: -const defaultContractConfig = (): ContractCreationConfig => ({ - contractAdmin: creatorAccount, +const defaultContractConfig = ({ + contractAdmin, +}: { + contractAdmin: Address; +}): ContractCreationConfig => ({ + contractAdmin, contractURI: "ipfs://asdfasdfasdf", contractName: "My fun NFT", }); -const defaultTokenConfig = (): TokenCreationConfig => ({ +const defaultTokenConfig = ( + fixedPriceMinterAddress: Address +): TokenCreationConfig => ({ tokenURI: "ipfs://tokenIpfsId0", maxSupply: 100n, maxTokensPerAddress: 10n, - pricePerToken: parseEther("0.1"), + pricePerToken: 0n, mintStart: 0n, mintDuration: 100n, royaltyMintSchedule: 30, royaltyBPS: 200, royaltyRecipient: creatorAccount, + fixedPriceMinter: fixedPriceMinterAddress, }); -const defaultPremintConfig = (): PremintConfig => ({ - contractConfig: defaultContractConfig(), - tokenConfig: defaultTokenConfig(), +const defaultPremintConfig = (fixedPriceMinter: Address): PremintConfig => ({ + tokenConfig: defaultTokenConfig(fixedPriceMinter), deleted: false, uid: 105, version: 0, }); -// const useForkContract = true; +const useForkContract = true; describe("ZoraCreator1155Preminter", () => { beforeEach(async (ctx) => { @@ -179,35 +186,54 @@ describe("ZoraCreator1155Preminter", () => { value: parseEther("10"), }); - // ctx.forkedChainId = zoraTestnet.id; + ctx.forkedChainId = zoraTestnet.id; ctx.anvilChainId = foundry.id; let preminterAddress: Address; - let zora1155Address: Address; - const deployed = await deployPreminterContract(); - - preminterAddress = deployed.preminterAddress; - zora1155Address = deployed.zora1155Address; + if (useForkContract) { + const factoryProxyAddress = + zoraCreator1155FactoryImplAddress[ctx.forkedChainId]; + ctx.fixedPriceMinterAddress = await publicClient.readContract({ + abi: zoraCreator1155FactoryImplConfig.abi, + address: zoraCreator1155FactoryImplAddress[ctx.forkedChainId], + functionName: "fixedPriceMinter", + }); + const deployed = await deployPreminterContract(factoryProxyAddress); + preminterAddress = deployed.preminterAddress; + } else { + const factoryProxyAddress = (await deployFactoryProxy()) + .factoryProxyAddress; + const deployed = await deployPreminterContract(factoryProxyAddress); + preminterAddress = deployed.preminterAddress; + } - ctx.zoraMintFee = await publicClient.readContract({ - abi: zoraCreator1155ImplABI, - address: zora1155Address, - functionName: "mintFee", - }); + ctx.zoraMintFee = parseEther("0.000777"); ctx.preminterAddress = preminterAddress; }, 20 * 1000); // skip for now - we need to make this work on zora testnet chain too it.skip( - "can sign for another chain", - async ({ preminterAddress: preminterAddress }) => { - const premintConfig = defaultPremintConfig(); + "can sign on the forked premint contract", + async ({ fixedPriceMinterAddress, forkedChainId }) => { + const premintConfig = defaultPremintConfig(fixedPriceMinterAddress); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); + + const preminterAddress = zoraCreator1155PremintExecutorAddress[forkedChainId as keyof typeof zoraCreator1155PremintExecutorAddress] as Address; + + const contractAddress = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getContractAddress", + args: [contractConfig], + }); const signedMessage = await walletClient.signTypedData({ ...preminterTypedDataDefinition({ - verifyingContract: preminterAddress, + verifyingContract: contractAddress, chainId: 999, premintConfig, }), @@ -217,30 +243,36 @@ describe("ZoraCreator1155Preminter", () => { console.log({ creatorAccount, signedMessage, + contractConfig, premintConfig, - contractAddress: await publicClient.readContract({ - abi: preminterAbi, - address: preminterAddress, - functionName: "getContractAddress", - args: [defaultContractConfig()], - }), - }); + contractAddress + }); }, 20 * 1000 ); it( "can sign and recover a signature", - async ({ preminterAddress: preminterAddress, anvilChainId }) => { - const premintConfig = defaultPremintConfig(); + async ({ + preminterAddress: preminterAddress, + anvilChainId, + fixedPriceMinterAddress, + }) => { + const premintConfig = defaultPremintConfig(fixedPriceMinterAddress); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); - console.log({ - defaultMind: defaultPremintConfig(), + const contractAddress = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getContractAddress", + args: [contractConfig], }); // sign message containing contract and token creation config and uid const signedMessage = await walletClient.signTypedData({ ...preminterTypedDataDefinition({ - verifyingContract: preminterAddress, + verifyingContract: contractAddress, // we need to sign here for the anvil chain, cause thats where it is run on chainId: anvilChainId, premintConfig, @@ -253,7 +285,7 @@ describe("ZoraCreator1155Preminter", () => { abi: preminterAbi, address: preminterAddress, functionName: "recoverSigner", - args: [premintConfig, signedMessage], + args: [premintConfig, contractAddress, signedMessage], }); expect(recoveredAddress).to.equal(creatorAccount); @@ -261,16 +293,19 @@ describe("ZoraCreator1155Preminter", () => { 20 * 1000 ); - it( "can sign and mint multiple tokens", async ({ zoraMintFee, anvilChainId, preminterAddress: preminterAddress, + fixedPriceMinterAddress, }) => { // setup contract and token creation parameters - const premintConfig = defaultPremintConfig(); + const premintConfig = defaultPremintConfig(fixedPriceMinterAddress); + const contractConfig = defaultContractConfig({ + contractAdmin: creatorAccount, + }); // lets make it a random number to not break the existing tests that expect fresh data premintConfig.uid = Math.round(Math.random() * 1000000); @@ -279,18 +314,18 @@ describe("ZoraCreator1155Preminter", () => { abi: preminterAbi, address: preminterAddress, functionName: "getContractAddress", - args: [premintConfig.contractConfig], + args: [contractConfig], }); // have creator sign the message to create the contract // and the token const signedMessage = await walletClient.signTypedData({ ...preminterTypedDataDefinition({ - verifyingContract: preminterAddress, + verifyingContract: contractAddress, + // we need to sign here for the anvil chain, cause thats where it is run on chainId: anvilChainId, premintConfig, }), - // signer account is the creator account: creatorAccount, }); @@ -304,19 +339,19 @@ describe("ZoraCreator1155Preminter", () => { await testClient.setBalance({ address: collectorAccount, - value: 10n * 10n ** 18n, + value: parseEther("10"), }); // get the premint status - it should not be minted - let tokenId = await publicClient.readContract({ + let [contractCreated, tokenId] = await publicClient.readContract({ abi: preminterAbi, address: preminterAddress, - functionName: "getPremintedTokenId", - args: [premintConfig.contractConfig, premintConfig.uid], + functionName: "premintStatus", + args: [contractAddress, premintConfig.uid], }); + expect(contractCreated).toBe(false); expect(tokenId).toBe(0n); - // expect(contractAddress).toBe(zeroAddress); // now have the collector execute the first signed message; // it should create the contract, the token, @@ -328,7 +363,13 @@ describe("ZoraCreator1155Preminter", () => { functionName: "premint", account: collectorAccount, address: preminterAddress, - args: [premintConfig, signedMessage, quantityToMint, comment], + args: [ + contractConfig, + premintConfig, + signedMessage, + quantityToMint, + comment, + ], value: valueToSend, }); @@ -336,25 +377,25 @@ describe("ZoraCreator1155Preminter", () => { const receipt = await publicClient.waitForTransactionReceipt({ hash: mintHash, }); - // console.log(receipt); + expect(receipt.status).toBe("success"); // fetch the premint token id - let newTokenId = await publicClient.readContract({ + [contractCreated, tokenId] = await publicClient.readContract({ abi: preminterAbi, address: preminterAddress, - functionName: "getPremintedTokenId", - args: [premintConfig.contractConfig, premintConfig.uid], + functionName: "premintStatus", + args: [contractAddress, premintConfig.uid], }); - expect(newTokenId).not.toBe(0n); + expect(tokenId).not.toBe(0n); // now use what was created, to get the balance from the created contract const tokenBalance = await publicClient.readContract({ abi: zoraCreator1155ImplABI, address: contractAddress, functionName: "balanceOf", - args: [collectorAccount, newTokenId], + args: [collectorAccount, tokenId], }); // get token balance - should be amount that was created @@ -373,7 +414,7 @@ describe("ZoraCreator1155Preminter", () => { // sign the message to create the second token const signedMessage2 = await walletClient.signTypedData({ ...preminterTypedDataDefinition({ - verifyingContract: preminterAddress, + verifyingContract: contractAddress, chainId: foundry.id, premintConfig: premintConfig2, }), @@ -393,7 +434,13 @@ describe("ZoraCreator1155Preminter", () => { functionName: "premint", account: collectorAccount, address: preminterAddress, - args: [premintConfig2, signedMessage2, quantityToMint2, comment], + args: [ + contractConfig, + premintConfig2, + signedMessage2, + quantityToMint2, + comment, + ], value: valueToSend2, }); @@ -403,11 +450,11 @@ describe("ZoraCreator1155Preminter", () => { ).toBe("success"); // now premint status for the second mint, it should be minted - tokenId = await publicClient.readContract({ + [, tokenId] = await publicClient.readContract({ abi: preminterAbi, address: preminterAddress, - functionName: "getPremintedTokenId", - args: [premintConfig2.contractConfig, premintConfig2.uid], + functionName: "premintStatus", + args: [contractAddress, premintConfig2.uid], }); expect(tokenId).not.toBe(0n); @@ -425,4 +472,5 @@ describe("ZoraCreator1155Preminter", () => { // 10 second timeout 40 * 1000 ); + }); diff --git a/package/preminter.ts b/package/preminter.ts index d4a35255f..6b6f4e12c 100644 --- a/package/preminter.ts +++ b/package/preminter.ts @@ -1,18 +1,17 @@ import { Address } from "abitype"; import { ExtractAbiFunction, AbiParametersToPrimitiveTypes } from "abitype"; -import { zoraCreator1155PreminterABI as preminterAbi } from "./wagmiGenerated"; +import { zoraCreator1155PremintExecutorABI as preminterAbi } from "./wagmiGenerated"; import { TypedDataDefinition } from "viem"; -type PreminterHashInputs = ExtractAbiFunction< +type PremintInputs = ExtractAbiFunction< typeof preminterAbi, - "premintHashData" + "premint" >["inputs"]; -type PreminterHashDataTypes = - AbiParametersToPrimitiveTypes; +type PreminterHashDataTypes = AbiParametersToPrimitiveTypes; -export type PremintConfig = PreminterHashDataTypes[0]; -export type ContractCreationConfig = PremintConfig["contractConfig"]; +export type ContractCreationConfig = PreminterHashDataTypes[0]; +export type PremintConfig = PreminterHashDataTypes[1]; export type TokenCreationConfig = PremintConfig["tokenConfig"]; // Convenience method to create the structured typed data @@ -26,20 +25,18 @@ export const preminterTypedDataDefinition = ({ premintConfig: PremintConfig; chainId: number; }) => { - const { contractConfig, tokenConfig, uid, version, deleted } = premintConfig; + const { tokenConfig, uid, version, deleted } = premintConfig; const types = { - Premint: [ - { name: "contractConfig", type: "ContractCreationConfig" }, + CreatorAttribution: [ { name: "tokenConfig", type: "TokenCreationConfig" }, + // unique id scoped to the contract and token to create. + // ensure that a signature can be replaced, as long as the replacement + // has the same uid, and a newer version. { name: "uid", type: "uint32" }, { name: "version", type: "uint32" }, + // if this update should result in the signature being deleted. { name: "deleted", type: "bool" }, ], - ContractCreationConfig: [ - { name: "contractAdmin", type: "address" }, - { name: "contractURI", type: "string" }, - { name: "contractName", type: "string" }, - ], TokenCreationConfig: [ { name: "tokenURI", type: "string" }, { name: "maxSupply", type: "uint256" }, @@ -50,25 +47,25 @@ export const preminterTypedDataDefinition = ({ { name: "royaltyMintSchedule", type: "uint32" }, { name: "royaltyBPS", type: "uint32" }, { name: "royaltyRecipient", type: "address" }, + { name: "fixedPriceMinter", type: "address" }, ], }; - const result: TypedDataDefinition = { + const result: TypedDataDefinition = { domain: { chainId, name: "Preminter", - version: "0.0.1", + version: "1", verifyingContract: verifyingContract, }, types, message: { - contractConfig, tokenConfig, uid, version, deleted, }, - primaryType: "Premint", + primaryType: "CreatorAttribution", }; // console.log({ result, deleted }); diff --git a/script/DeployPreminter.s.sol b/script/DeployPreminter.s.sol index cc9b97d47..4126fd9b1 100644 --- a/script/DeployPreminter.s.sol +++ b/script/DeployPreminter.s.sol @@ -18,7 +18,7 @@ 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"; +import {ZoraCreator1155PremintExecutor} from "../src/premint/ZoraCreator1155PremintExecutor.sol"; contract DeployPreminter is ZoraDeployerBase { function run() public returns (string memory) { @@ -26,12 +26,50 @@ contract DeployPreminter is ZoraDeployerBase { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - ZoraCreator1155FactoryImpl factory = ZoraCreator1155FactoryImpl(deployment.factoryProxy); + // bool deployFactory = vm.envBool("DEPLOY_FACTORY"); + bool deployFactory = vm.envBool("DEPLOY_FACTORY"); + IZoraCreator1155Factory factoryProxy; vm.startBroadcast(deployerPrivateKey); - ZoraCreator1155Preminter preminter = new ZoraCreator1155Preminter(); - preminter.initialize(factory); + if (deployFactory) { + address deployer = vm.envAddress("DEPLOYER"); + address factoryShimAddress = address(new ProxyShim(deployer)); + ChainConfig memory chainConfig = getChainConfig(); + + factoryProxy = IZoraCreator1155Factory(address(new Zora1155Factory(factoryShimAddress, ""))); + + deployment.factoryProxy = address(factoryProxy); + + ZoraCreator1155Impl creatorImpl = new ZoraCreator1155Impl( + chainConfig.mintFeeAmount, + chainConfig.mintFeeRecipient, + address(factoryProxy), + chainConfig.protocolRewards + ); + + deployment.contract1155Impl = address(creatorImpl); + + ZoraCreator1155FactoryImpl factoryImpl = new ZoraCreator1155FactoryImpl({ + _implementation: creatorImpl, + _merkleMinter: IMinter1155(deployment.merkleMintSaleStrategy), + _redeemMinterFactory: IMinter1155(deployment.redeemMinterFactory), + _fixedPriceMinter: IMinter1155(deployment.fixedPriceSaleStrategy) + }); + + // Upgrade to "real" factory address + ZoraCreator1155FactoryImpl(address(factoryProxy)).upgradeTo(address(factoryImpl)); + ZoraCreator1155FactoryImpl(address(factoryProxy)).initialize(chainConfig.factoryOwner); + + deployment.factoryImpl = address(factoryImpl); + } else { + factoryProxy = ZoraCreator1155FactoryImpl(deployment.factoryProxy); + } + + console.log("!!!factory proxy!!!"); + // console.log(factoryProxy); + + ZoraCreator1155PremintExecutor preminter = new ZoraCreator1155PremintExecutor(factoryProxy); vm.stopBroadcast(); 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..d8b79e2b9 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/ZoraCreator1155Attribution.sol"; /* @@ -104,6 +105,10 @@ 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 delegatedTokenId(uint32 uid) external view returns (uint256 tokenId); + 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 d9638473b..ee46e35c3 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, PremintTokenSetup, PremintConfig} from "../premint/ZoraCreator1155Attribution.sol"; /// Imagine. Mint. Enjoy. /// @title ZoraCreator1155Impl @@ -722,4 +723,46 @@ contract ZoraCreator1155Impl is revert(); } } + + /* start eip712 functionality */ + mapping(uint32 => uint256) public delegatedTokenId; + + event CreatorAttribution(bytes32 structHash, bytes32 domainName, bytes32 version, bytes signature); + + error PremintAlreadyExecuted(); + + function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature) public nonReentrant returns (uint256 newTokenId) { + bytes32 hashedPremintConfig = ZoraCreator1155Attribution.validateAndHashPremint(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); + + // check that uid hasn't been used + if (delegatedTokenId[premintConfig.uid] != 0) { + revert PremintAlreadyExecuted(); + } + + // 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); + + delegatedTokenId[premintConfig.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, recoveredSigner, premintConfig.tokenConfig); + + // then invoke them, calling account should be original msg.sender, which has admin on the new token + _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/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol new file mode 100644 index 000000000..5efab4f8b --- /dev/null +++ b/src/premint/ZoraCreator1155Attribution.sol @@ -0,0 +1,237 @@ +// 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 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 + ) 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); + } + + 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 ATTRIBUTION_DOMAIN = + 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) { + return + keccak256(abi.encode(ATTRIBUTION_DOMAIN, _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,address fixedPriceMinter)" + ); + + 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, + tokenConfig.fixedPriceMinter + ) + ); + } + + function _stringHash(string calldata value) private pure returns (bytes32) { + return keccak256(bytes(value)); + } + + // todo: move to its own contract + error MintNotYetStarted(); + error PremintDeleted(); + + function validateAndHashPremint(PremintConfig calldata premintConfig) external view returns (bytes32) { + 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(); + } + + return hashPremint(premintConfig); + } +} + +library PremintTokenSetup { + 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/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol new file mode 100644 index 000000000..f615940f4 --- /dev/null +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; +import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.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, ZoraCreator1155Attribution} from "./ZoraCreator1155Attribution.sol"; + +/// @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 ZoraCreator1155PremintExecutor { + IZoraCreator1155Factory factory; + + /// @notice copied from SharedBaseConstants + uint256 constant CONTRACT_BASE_ID = 0; + /// @dev copied from ZoraCreator1155Impl + uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; + + error MintNotYetStarted(); + error InvalidSignature(); + + constructor(IZoraCreator1155Factory _factory) { + factory = _factory; + } + + 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. + /// 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. + /// @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( + ContractCreationConfig calldata contractConfig, + PremintConfig 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); + address contractAddress = address(tokenContract); + + // 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); + + // mint the initial x tokens for this new token id to the executor. + address tokenRecipient = msg.sender; + + tokenContract.mint{value: msg.value}( + IMinter1155(premintConfig.tokenConfig.fixedPriceMinter), + newTokenId, + quantityToMint, + abi.encode(tokenRecipient, mintComment) + ); + + // emit Preminted event + emit Preminted(contractAddress, 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 doesnt exist for hash, createi t + tokenContract = _createContract(contractConfig); + } else { + tokenContract = IZoraCreator1155(contractAddress); + } + } + + 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 = factory.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 onthe 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 factory.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); + } + + /// @notice Utility function to determine if a premint contract has been created for a uid of a premint, and if so, + /// What is the token id that was created for the uid. + function premintStatus(address contractAddress, uint32 uid) public view returns (bool contractCreated, uint256 tokenIdForPremint) { + if (contractAddress.code.length == 0) { + return (false, 0); + } + return (true, IZoraCreator1155(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. + 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)); + } + + // 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); + } + } +} diff --git a/src/premint/ZoraCreator1155Preminter.sol b/src/premint/ZoraCreator1155Preminter.sol deleted file mode 100644 index 3f27818e6..000000000 --- a/src/premint/ZoraCreator1155Preminter.sol +++ /dev/null @@ -1,366 +0,0 @@ -// SPDX-License-Identifier: MIT -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"; - -/// @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 { - 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"); - 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( - address indexed contractAddress, - uint256 indexed tokenId, - bool indexed createdNewContract, - uint32 uid, - ContractCreationConfig contractConfig, - TokenCreationConfig tokenConfig, - address minter, - uint256 quantityMinted - ); - - // same signature should work whether or not there is an existing contract - // so it is unaware of order, it just takes the token uri and creates the next token with it - // 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) { - // 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 - // 4. Mint a new token, and get the new token id - // 5. Setup fixed price minting rules for the new token - // 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(); - } - - // 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); - - // 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)); - } - - 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 doesnt exist for hash, createi t - tokenContract = _createContract(contractConfig); - } else { - tokenContract = IZoraCreator1155(contractAddress); - } - } - - 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); - - // create the contract via the factory. - address newContractAddresss = factory.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); - } - - 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 2b6291846..399b5985e 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -16,11 +16,13 @@ import {ILimitedMintPerAddress} from "../../src/interfaces/ILimitedMintPerAddres import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; -import {ZoraCreator1155Preminter} from "../../src/premint/ZoraCreator1155Preminter.sol"; +import {ZoraCreator1155PremintExecutor} from "../../src/premint/ZoraCreator1155PremintExecutor.sol"; import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Attribution.sol"; +import {ForkDeploymentConfig} from "../../src/deployment/DeploymentConfig.sol"; -contract ZoraCreator1155PreminterTest is Test { - ZoraCreator1155Preminter internal preminter; +contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { + ZoraCreator1155PremintExecutor internal preminter; ZoraCreator1155FactoryImpl internal factory; // setup contract config uint256 creatorPrivateKey = 0xA11CE; @@ -33,8 +35,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 ); @@ -54,20 +56,20 @@ contract ZoraCreator1155PreminterTest is Test { royaltyMintSchedule: royaltyMintSchedule }); - preminter = new ZoraCreator1155Preminter(); - preminter.initialize(factory); + preminter = new ZoraCreator1155PremintExecutor(factory); creatorPrivateKey = 0xA11CE; 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, @@ -76,49 +78,48 @@ 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) { - return - ZoraCreator1155Preminter.PremintConfig({ - contractConfig: makeDefaultContractCreationConfig(), - tokenConfig: makeDefaultTokenCreationConfig(), - uid: 100, - version: 0, - deleted: false - }); + function makeDefaultPremintConfig() internal view returns (PremintConfig memory) { + return PremintConfig({tokenConfig: makeDefaultTokenCreationConfig(), uid: 100, version: 0, deleted: false}); } function test_successfullyMintsTokens() external { // 1. Make contract creation params // configuration of contract to create - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + 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(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 bytes memory signature = _sign(creatorPrivateKey, digest); + uint256 mintFee = 0; + + uint256 mintCost = mintFee * quantityToMint; + // this account will be used to execute the premint, and should result in a contract being created address premintExecutor = vm.addr(701); - + vm.deal(premintExecutor, mintCost); // 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{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); // get the contract address from the preminter based on the contract hash id. IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); @@ -131,23 +132,93 @@ 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{value: mintCost}(contractConfig, 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 assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), quantityToMint); } + /// @notice gets the chains to do fork tests on, by reading environment var FORK_TEST_CHAINS. + /// Chains are by name, and must match whats under `rpc_endpoints` in the foundry.toml + function getForkTestChains() private view returns (string[] memory result) { + try vm.envString("FORK_TEST_CHAINS", ",") returns (string[] memory forkTestChains) { + result = forkTestChains; + } catch { + console.log("could not get fork test chains - make sure the environment variable FORK_TEST_CHAINS is set"); + result = new string[](0); + } + } + + function testTheForkPremint(string memory chainName) private { + console.log("testing on fork: ", chainName); + + // create and select the fork, which will be used for all subsequent calls + // it will also affect the current block chain id based on the rpc url returned + vm.createSelectFork(vm.rpcUrl(chainName)); + + // get contract hash, which is unique per contract creation config, and can be used + // retreive the address created for a contract + preminter = ZoraCreator1155PremintExecutor(getDeployment().preminter); + factory = ZoraCreator1155FactoryImpl(getDeployment().factoryImpl); + + console.log("building defaults"); + + // configuration of contract to create + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig 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); + + console.log(contractAddress); + + // 2. Call smart contract to get digest to sign for creation params. + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + + // 3. Sign the digest + // create a signature with the digest for the params + bytes memory signature = _sign(creatorPrivateKey, digest); + + // this account will be used to execute the premint, and should result in a contract being created + address premintExecutor = vm.addr(701); + uint256 mintCost = quantityToMint * 0.000777 ether; + // 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); + + // 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_fork_successfullyMintsTokens() external { + string[] memory forkTestChains = getForkTestChains(); + for (uint256 i = 0; i < forkTestChains.length; i++) { + testTheForkPremint(forkTestChains[i]); + } + } + function test_signatureForSameContractandUid_cannotBeExecutedTwice() external { // 1. Make contract creation params // configuration of contract to create - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -155,34 +226,36 @@ contract ZoraCreator1155PreminterTest is Test { address premintExecutor = vm.addr(701); string memory comment = "I love it"; - _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); + address contractAddress = preminter.getContractAddress(contractConfig); + _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); // create a sig for another token with same uid, it should revert premintConfig.tokenConfig.tokenURI = "blah2.token"; - bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); vm.startPrank(premintExecutor); // premint with new token config and signature - it should revert - vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Preminter.PremintAlreadyExecuted.selector)); - preminter.premint(premintConfig, signature, quantityToMint, comment); + vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Impl.PremintAlreadyExecuted.selector)); + preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); // change the version, it should still revert premintConfig.version++; - signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); // premint with new token config and signature - it should revert - vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Preminter.PremintAlreadyExecuted.selector)); - preminter.premint(premintConfig, signature, quantityToMint, comment); + vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Impl.PremintAlreadyExecuted.selector)); + preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); // change the uid, it should not revert premintConfig.uid++; - signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - preminter.premint(premintConfig, signature, quantityToMint, comment); + preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); } function test_deleted_preventsTokenFromBeingMinted() external { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); premintConfig.deleted = true; uint chainId = block.chainid; @@ -190,32 +263,35 @@ contract ZoraCreator1155PreminterTest is Test { uint256 quantityToMint = 2; string memory comment = "I love it"; + address contractAddress = preminter.getContractAddress(contractConfig); + // 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(contractAddress, premintConfig, creatorPrivateKey, chainId); - assertEq(contractAddress, address(0)); - assertEq(tokenId, 0); + // now call the premint function, using the same config that was used to generate the digest, and the signature + vm.expectRevert(ZoraCreator1155Attribution.PremintDeleted.selector); + vm.prank(premintExecutor); + uint256 newTokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + + assertEq(newTokenId, 0, "tokenId"); // make sure no contract was created - assertEq(preminter.getContractAddress(premintConfig.contractConfig).code.length, 0); + assertEq(contractAddress.code.length, 0, "contract has been deployed"); } function test_emitsPremint_whenNewContract() external { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); + address contractAddress = preminter.getContractAddress(contractConfig); // how many tokens are minted to the executor uint256 quantityToMint = 4; uint256 chainId = block.chainid; // Sign the premint - bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); + + uint256 expectedTokenId = 1; // this account will be used to execute the premint, and should result in a contract being created address premintExecutor = vm.addr(701); @@ -224,31 +300,24 @@ contract ZoraCreator1155PreminterTest is Test { vm.startPrank(premintExecutor); - // 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); - 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); emit Preminted( contractAddress, - tokenId, + expectedTokenId, createdNewContract, premintConfig.uid, - premintConfig.contractConfig, + contractConfig, premintConfig.tokenConfig, premintExecutor, quantityToMint ); - preminter.premint(premintConfig, signature, quantityToMint, comment); + preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); } function test_onlyOwner_hasAdminRights_onCreatedToken() public { - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor uint256 quantityToMint = 4; @@ -257,14 +326,9 @@ contract ZoraCreator1155PreminterTest is Test { address premintExecutor = vm.addr(701); string memory comment = "I love it"; - (address createdContractAddress, uint256 newTokenId) = _signAndExecutePremint( - premintConfig, - creatorPrivateKey, - chainId, - premintExecutor, - quantityToMint, - comment - ); + address createdContractAddress = preminter.getContractAddress(contractConfig); + + uint256 newTokenId = _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); // get the contract address from the preminter based on the contract hash id. IZoraCreator1155 created1155Contract = IZoraCreator1155(createdContractAddress); @@ -322,7 +386,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; @@ -334,14 +398,16 @@ 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 = makeDefaultContractCreationConfig(); + ContractCreationConfig memory secondContractConfig = ContractCreationConfig( firstContractConfig.contractAdmin, firstContractConfig.contractURI, string.concat(firstContractConfig.contractName, "4") ); - (address resultContractAddress, uint256 newTokenId) = _signAndExecutePremint( + address firstContractAddress = preminter.getContractAddress(firstContractConfig); + uint256 firstResultTokenId = _signAndExecutePremint( + firstContractConfig, premintConfig, creatorPrivateKey, chainId, @@ -349,27 +415,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(firstContractAddress).balanceOf(premintExecutor, firstResultTokenId), quantityToMint); premintConfig.uid = secondUid; - (resultContractAddress, newTokenId) = _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - tokenId = preminter.getPremintedTokenId(firstContractConfig, secondUid); - - assertEq(contractAddress, resultContractAddress); - assertEq(tokenId, newTokenId); + uint256 secondResultTokenId = _signAndExecutePremint( + firstContractConfig, + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); - premintConfig.contractConfig = secondContractConfig; + assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, secondResultTokenId), quantityToMint); - (resultContractAddress, newTokenId) = _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - contractAddress = preminter.getContractAddress(secondContractConfig); - tokenId = preminter.getPremintedTokenId(secondContractConfig, secondUid); + address secondContractAddress = preminter.getContractAddress(secondContractConfig); + uint256 thirdResultTokenId = _signAndExecutePremint( + secondContractConfig, + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); - assertEq(contractAddress, resultContractAddress); - assertEq(tokenId, newTokenId); + assertEq(IZoraCreator1155(secondContractAddress).balanceOf(premintExecutor, thirdResultTokenId), quantityToMint); } function test_premintCanOnlyBeExecutedAfterStartDate(uint8 startDate, uint8 currentTime) external { @@ -382,7 +455,8 @@ contract ZoraCreator1155PreminterTest is Test { } vm.warp(currentTime); - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); premintConfig.tokenConfig.mintStart = startDate; uint256 quantityToMint = 4; @@ -391,13 +465,13 @@ contract ZoraCreator1155PreminterTest is Test { string memory comment = "I love it"; // get signature for the premint: - bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, creatorPrivateKey, chainId); if (shouldRevert) { - vm.expectRevert(ZoraCreator1155Preminter.MintNotYetStarted.selector); + vm.expectRevert(ZoraCreator1155Attribution.MintNotYetStarted.selector); } vm.prank(premintExecutor); - preminter.premint(premintConfig, signature, quantityToMint, comment); + preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); } function test_premintCanOnlyBeExecutedUpToDurationFromFirstMint(uint8 startDate, uint8 duration, uint8 timeOfFirstMint, uint8 timeOfSecondMint) external { @@ -413,14 +487,17 @@ contract ZoraCreator1155PreminterTest is Test { } // build a premint with a token that has the given start date and duration - ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); + address contractAddress = preminter.getContractAddress(contractConfig); + premintConfig.tokenConfig.mintStart = startDate; premintConfig.tokenConfig.mintDuration = duration; uint256 chainId = block.chainid; // get signature for the premint: - bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); uint256 quantityToMint = 2; address premintExecutor = vm.addr(701); @@ -429,7 +506,7 @@ contract ZoraCreator1155PreminterTest is Test { vm.startPrank(premintExecutor); vm.warp(timeOfFirstMint); - (address contractAddress, uint256 tokenId) = preminter.premint(premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); vm.warp(timeOfSecondMint); @@ -438,30 +515,114 @@ contract ZoraCreator1155PreminterTest is Test { if (shouldRevert) { vm.expectRevert(ZoraCreatorFixedPriceSaleStrategy.SaleEnded.selector); } + IZoraCreator1155(contractAddress).mint(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment)); } + function test_premintStatus_getsIfContractHasBeenCreatedAndTokenIdForPremint() external { + // build a premint + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // get premint status + (bool contractCreated, uint256 tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid); + // contract should not be created and token id should be 0 + assertEq(contractCreated, false); + assertEq(tokenId, 0); + + // sign and execute premint + uint256 newTokenId = _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, vm.addr(701), 1, "hi"); + + // get status + (contractCreated, tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid); + // contract should be created and token id should be same as one that was created + assertEq(contractCreated, true); + assertEq(tokenId, newTokenId); + + // get status for another uid + (contractCreated, tokenId) = preminter.premintStatus(preminter.getContractAddress(contractConfig), premintConfig.uid + 1); + // contract should be created and token id should be 0 + assertEq(contractCreated, true); + assertEq(tokenId, 0); + } + + // todo: pull from elsewhere + uint256 constant CONTRACT_BASE_ID = 0; + uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; + + function test_premint_whenContractCreated_premintCanOnlyBeExecutedByPermissionBitMinter() external { + // build a premint + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + address executor = vm.addr(701); + + // sign and execute premint + bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, creatorPrivateKey, block.chainid); + + (bool isValidSignature, address contractAddress, ) = preminter.isValidSignature(contractConfig, premintConfig, signature); + + assertTrue(isValidSignature); + + _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, executor, 1, "hi"); + + // contract has been created + + // have another creator sign a premint + uint256 newCreatorPrivateKey = 0xA11CF; + address newCreator = vm.addr(newCreatorPrivateKey); + PremintConfig 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); + + assertFalse(isValidSignature); + + // try to mint, it should revert + vm.expectRevert(abi.encodeWithSelector(IZoraCreator1155.UserMissingRoleForToken.selector, newCreator, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER)); + vm.prank(executor); + preminter.premint(contractConfig, premintConfig2, newCreatorSignature, 1, "yo"); + + // 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); + + // try to mint again, should not revert + vm.prank(executor); + preminter.premint(contractConfig, premintConfig2, newCreatorSignature, 1, "yo"); + } + function _signAndExecutePremint( - ZoraCreator1155Preminter.PremintConfig memory premintConfig, + ContractCreationConfig memory contractConfig, + PremintConfig memory premintConfig, uint256 privateKey, uint256 chainId, address executor, uint256 quantityToMint, string memory comment - ) private returns (address, uint256) { - bytes memory signature = _signPremint(premintConfig, privateKey, chainId); + ) private returns (uint256 newTokenId) { + bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, privateKey, chainId); // now call the premint function, using the same config that was used to generate the digest, and the signature vm.prank(executor); - return preminter.premint(premintConfig, signature, quantityToMint, comment); + newTokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); } function _signPremint( - ZoraCreator1155Preminter.PremintConfig memory premintConfig, + address contractAddress, + PremintConfig memory premintConfig, uint256 privateKey, uint256 chainId - ) private view returns (bytes memory) { - bytes32 digest = preminter.premintHashData(premintConfig, address(preminter), chainId); + ) private pure returns (bytes memory) { + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); // 3. Sign the digest // create a signature with the digest for the params diff --git a/uml/gasslessCreate-collecting-sequence.puml b/uml/gasslessCreate-collecting-sequence.puml index 2d029e013..2eccc4c56 100644 --- a/uml/gasslessCreate-collecting-sequence.puml +++ b/uml/gasslessCreate-collecting-sequence.puml @@ -7,9 +7,9 @@ entity PreminterContract entity 1155FactoryContract entity 1155Contract -Collector -> PremintCollectPage: Open, param is \ncontract hash + token uid +Collector -> PremintCollectPage: Open, param is \ndeterministic collection address\n+ token uid Activate PremintCollectPage -PremintCollectPage -> SignatureAPI: Fetch by contract hash + token uid +PremintCollectPage -> SignatureAPI: Fetch by collection address\n+ token uid SignatureAPI -> SignatureDB: Fetch most recent signature\nby contract hash token uid SignatureDB --> SignatureAPI: contract + token creation params\n+ signature SignatureAPI --> PremintCollectPage: contract + token creation params\n+ signature @@ -37,8 +37,7 @@ Group contract doesnt exist end -PreminterContract -> 1155Contract: create new token -PreminterContract -> 1155Contract: set new token sale parameters +PreminterContract -> 1155Contract: create new token\nwith signature PreminterContract -> 1155Contract: mint tokens to collector deactivate PreminterContract diff --git a/uml/gasslessCreate-creation-activity.puml b/uml/gasslessCreate-creation-activity.puml index 201f388da..363927823 100644 --- a/uml/gasslessCreate-creation-activity.puml +++ b/uml/gasslessCreate-creation-activity.puml @@ -12,15 +12,15 @@ if (new token) then (yes) else (no) endif else (no) - :load existing\ncontract creation parameters\nby hash id; + :load existing\ncontract creation parameters\nby collection address; endif :Get new uid\nfrom backend server; else (no) - :load existing\ncontract + token creation parameters\nby contract hash + uid; + :load existing\ncontract + token creation parameters\nby collection address + uid; endif :Ask creator for new\ntoken creation params; -:Request signature with:\ncontract + token params + uid; -:Submit to backend server:\ncontract + token params + uid + signature\n; +:Request signature with:\ncollection address + token params + uid; +:Submit to backend server:\ncollection + token params + uid + signature\n; stop diff --git a/uml/gasslessCreate-creation-sequence.puml b/uml/gasslessCreate-creation-sequence.puml index 2d7bc99fa..1d7213f43 100644 --- a/uml/gasslessCreate-creation-sequence.puml +++ b/uml/gasslessCreate-creation-sequence.puml @@ -5,6 +5,7 @@ title Creating a signature for a new erc1155 contract + token actor Creator entity CreatePage boundary SignatureAPI +boundary PremintContract entity SignatureDB @@ -20,22 +21,31 @@ end Group Signature has been created for contract - Creator -> CreatePage: load page by contract hash - CreatePage -> SignatureAPI: load contract creation params - SignatureAPI -> SignatureDB: fetch contract creation params\nby hash + Creator -> CreatePage: load page by determinstic collection address + CreatePage -> SignatureAPI: load collection creation params + SignatureAPI -> SignatureDB: fetch collection creation params\nby hash SignatureAPI --> CreatePage: contract creation params end Creator -> CreatePage: setup new token -Creator -> CreatePage: submit contract & token creation params -CreatePage -> SignatureAPI: get new uid for token -SignatureAPI -> SignatureDB: get next token uid\nscoped to contract hash +CreatePage -> PremintContract: get determnistic collection address +PremintContract --> CreatePage: determinstic collection address +CreatePage -> SignatureAPI: get new uid for collection address +SignatureAPI -> SignatureDB: get next token uid\nscoped to collection address SignatureDB --> SignatureAPI: next token uid SignatureAPI --> CreatePage: next token uid +Creator -> CreatePage: Submit new token creation params CreatePage -> Creator: request signature of\n contract + token creation params + token uid deactivate CreatePage Creator -> SignatureAPI: Submit signature + contract + token params + token uid -SignatureAPI -> SignatureDB: store signature + \ncontract creation + \ntoken creation params +\ntoken uid +SignatureAPI -> PremintContract: validate signature +PremintContract --> SignatureAPI: validation results (true/false & recovered signer) + +Group Signature is valid + + SignatureAPI -> SignatureDB: store signature + \ncontract creation + \ntoken creation params + \ncollection address + \ntoken uid + +end @enduml \ No newline at end of file diff --git a/uml/generated/gasslessCreate-collecting-sequence.svg b/uml/generated/gasslessCreate-collecting-sequence.svg index 87f3d5eed..0115a8fed 100644 --- a/uml/generated/gasslessCreate-collecting-sequence.svg +++ b/uml/generated/gasslessCreate-collecting-sequence.svg @@ -1 +1 @@ -CollectorCollectorPremintCollectPagePremintCollectPageSignatureAPISignatureAPISignatureDBSignatureDBPreminterContractPreminterContract1155FactoryContract1155FactoryContract1155Contract1155ContractOpen, param iscontract hash + token uidFetch by contract hash + token uidFetch most recent signatureby contract hash token uidcontract + token creation params+ signaturecontract + token creation params+ signatureCheck if signature has been used (by contract hash + token uid)Signature has been used or notsignature has been usedRedirect tostandard collect pagemintSubmit transactionSubmit premint transaction containingsignature, contract creation & token creation paramsrecord signature used;revert if already usedcontract doesnt existcreate contractcreatecreate new tokenset new token sale parametersmint tokens to collectorMinted tokens \ No newline at end of file +CollectorCollectorPremintCollectPagePremintCollectPageSignatureAPISignatureAPISignatureDBSignatureDBPreminterContractPreminterContract1155FactoryContract1155FactoryContract1155Contract1155ContractOpen, param isdeterministic collection address+ token uidFetch by collection address+ token uidFetch most recent signatureby contract hash token uidcontract + token creation params+ signaturecontract + token creation params+ signatureCheck if signature has been used (by contract hash + token uid)Signature has been used or notsignature has been usedRedirect tostandard collect pagemintSubmit transactionSubmit premint transaction containingsignature, contract creation & token creation paramsrecord signature used;revert if already usedcontract doesnt existcreate contractcreatecreate new tokenwith signaturemint tokens to collectorMinted tokens \ No newline at end of file diff --git a/uml/generated/gasslessCreate-creation-activity.svg b/uml/generated/gasslessCreate-creation-activity.svg index 6dbd1626a..b0e6d67cc 100644 --- a/uml/generated/gasslessCreate-creation-activity.svg +++ b/uml/generated/gasslessCreate-creation-activity.svg @@ -1 +1 @@ -Creating a token signaturenew tokenyesnonew contractyesnoAsk creator for newcontract creation paramsswitch ui to create tokenon existing contractyescontract existswith same paramsnoload existingcontract creation parametersby hash idGet new uidfrom backend serverload existingcontract + token creation parametersby contract hash + uidAsk creator for newtoken creation paramsRequest signature with:contract + token params + uidSubmit to backend server:contract + token params + uid + signature  \ No newline at end of file +Creating a token signaturenew tokenyesnonew contractyesnoAsk creator for newcontract creation paramsswitch ui to create tokenon existing contractyescontract existswith same paramsnoload existingcontract creation parametersby collection addressGet new uidfrom backend serverload existingcontract + token creation parametersby collection address + uidAsk creator for newtoken creation paramsRequest signature with:collection address + token params + uidSubmit to backend server:collection + token params + uid + signature  \ No newline at end of file diff --git a/uml/generated/gasslessCreate-creation-sequence.svg b/uml/generated/gasslessCreate-creation-sequence.svg index 9850b4315..4d2fbdad9 100644 --- a/uml/generated/gasslessCreate-creation-sequence.svg +++ b/uml/generated/gasslessCreate-creation-sequence.svg @@ -1 +1 @@ -Creating a signature for a new erc1155 contract + tokenCreatorCreatorCreatePageCreatePageSignatureAPISignatureAPISignatureDBSignatureDBSignature not created for contract yetsetup NEW contract name + imagevalidate that contractwith same params forcreator doesnt existcheck if signature with hashfor contract is already storedvalidation resultsSignature has been created for contractload page by contract hashload contract creation paramsfetch contract creation paramsby hashcontract creation paramssetup new tokensubmit contract & token creation paramsget new uid for tokenget next token uidscoped to contract hashnext token uidnext token uidrequest signature ofcontract + token creation params + token uidSubmit signature + contract + token params + token uidstore signature +contract creation +token creation params +token uid \ No newline at end of file +Creating a signature for a new erc1155 contract + tokenCreatorCreatorCreatePageCreatePageSignatureAPISignatureAPIPremintContractPremintContractSignatureDBSignatureDBSignature not created for contract yetsetup NEW contract name + imagevalidate that contractwith same params forcreator doesnt existcheck if signature with hashfor contract is already storedvalidation resultsSignature has been created for contractload page by determinstic collection addressload collection creation paramsfetch collection creation paramsby hashcontract creation paramssetup new tokenget determnistic collection addressdeterminstic collection addressget new uid for collection addressget next token uidscoped to collection addressnext token uidnext token uidSubmit new token creation paramsrequest signature ofcontract + token creation params + token uidSubmit signature + contract + token params + token uidvalidate signaturevalidation results (true/false & recovered signer)Signature is validstore signature +contract creation +token creation params +collection address +token uid \ No newline at end of file diff --git a/wagmi.config.ts b/wagmi.config.ts index 0733ea6c4..8c2d131b1 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -9,7 +9,7 @@ type ContractNames = | "ZoraCreatorMerkleMinterStrategy" | "ZoraCreatorRedeemMinterFactory" | "ZoraCreatorRedeemMinterStrategy" - | "ZoraCreator1155Preminter"; + | "ZoraCreator1155PremintExecutor"; type Address = `0x${string}`; @@ -20,7 +20,7 @@ const contractFilesToInclude: ContractNames[] = [ "ZoraCreatorMerkleMinterStrategy", "ZoraCreatorRedeemMinterFactory", "ZoraCreatorRedeemMinterStrategy", - "ZoraCreator1155Preminter", + "ZoraCreator1155PremintExecutor", ]; type Addresses = { @@ -82,7 +82,11 @@ const getAddresses = () => { chainId, jsonAddress.REDEEM_MINTER_FACTORY ); - addAddress("ZoraCreator1155Preminter", chainId, jsonAddress.PREMINTER); + addAddress( + "ZoraCreator1155PremintExecutor", + chainId, + jsonAddress.PREMINTER + ); } return addresses;