From ab0634413dccc0a84c5ae31077951b6d72df7ae7 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 16 Aug 2023 11:45:33 -0700 Subject: [PATCH 01/18] upgrade viem --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index d96f0959b..117e0ff7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,9 +645,9 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*": - version "20.4.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.10.tgz#73c9480791e3ddeb4887a660fc93a7f59353ad45" - integrity sha512-vwzFiiy8Rn6E0MtA13/Cxxgpan/N6UeNYR9oUu6kuJWxu6zCk98trcDp8CBhbtaeuq9SykCmXkFr2lWLoPcvLg== + version "20.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313" + integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q== "@types/node@^12.7.1": version "12.20.55" @@ -2976,9 +2976,9 @@ postcss-load-config@^3.0.1: yaml "^1.10.2" postcss@^8.4.27: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + version "8.4.28" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5" + integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From a3a59b85bb54d49f96a37dab060b82d98cb1bc63 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 31 May 2023 11:48:39 -0700 Subject: [PATCH 02/18] Gasless --- .changeset/long-avocados-visit.md | 6 + .env.anvil | 2 + .gitignore | 6 +- package.json | 7 +- package/batchPublish.test.ts | 2 +- package/preminter.test.ts | 428 ++++++++++++++++ package/preminter.ts | 77 +++ remappings.txt | 3 +- script/DeployPreminter.s.sol | 42 ++ script/EstimatePreminterGas.s.sol | 90 ++++ script/TestCreateDeterministic.sol | 68 +++ script/ZoraDeployerBase.sol | 1 + script/copy-deployed-contracts.mjs | 18 +- src/deployment/DeploymentConfig.sol | 4 + src/factory/ZoraCreator1155FactoryImpl.sol | 58 ++- src/interfaces/IZoraCreator1155Factory.sol | 16 + src/premint/EIP712UpgradeableWithChainId.sol | 106 ++++ src/premint/README.md | 59 +++ src/premint/ZoraCreator1155Preminter.sol | 366 ++++++++++++++ test/factory/ZoraCreator1155Factory.t.sol | 122 +++++ test/premint/ZoraCreator1155Preminter.t.sol | 478 ++++++++++++++++++ uml/gasslessCreate-collecting-activity.puml | 20 + uml/gasslessCreate-collecting-sequence.puml | 48 ++ uml/gasslessCreate-creation-activity.puml | 27 + uml/gasslessCreate-creation-sequence.puml | 41 ++ .../gasslessCreate-collecting-activity.svg | 1 + .../gasslessCreate-collecting-sequence.svg | 1 + .../gasslessCreate-creation-activity.svg | 1 + .../gasslessCreate-creation-sequence.svg | 1 + wagmi.config.ts | 9 +- yarn.lock | 173 +++---- 31 files changed, 2161 insertions(+), 120 deletions(-) create mode 100644 .changeset/long-avocados-visit.md create mode 100644 .env.anvil create mode 100644 package/preminter.test.ts create mode 100644 package/preminter.ts create mode 100644 script/DeployPreminter.s.sol create mode 100644 script/EstimatePreminterGas.s.sol create mode 100644 script/TestCreateDeterministic.sol create mode 100644 src/premint/EIP712UpgradeableWithChainId.sol create mode 100644 src/premint/README.md create mode 100644 src/premint/ZoraCreator1155Preminter.sol create mode 100644 test/premint/ZoraCreator1155Preminter.t.sol create mode 100644 uml/gasslessCreate-collecting-activity.puml create mode 100644 uml/gasslessCreate-collecting-sequence.puml create mode 100644 uml/gasslessCreate-creation-activity.puml create mode 100644 uml/gasslessCreate-creation-sequence.puml create mode 100644 uml/generated/gasslessCreate-collecting-activity.svg create mode 100644 uml/generated/gasslessCreate-collecting-sequence.svg create mode 100644 uml/generated/gasslessCreate-creation-activity.svg create mode 100644 uml/generated/gasslessCreate-creation-sequence.svg diff --git a/.changeset/long-avocados-visit.md b/.changeset/long-avocados-visit.md new file mode 100644 index 000000000..49e7233eb --- /dev/null +++ b/.changeset/long-avocados-visit.md @@ -0,0 +1,6 @@ +--- +"@zoralabs/zora-1155-contracts": minor +--- + +- Added gasless minter +- Added deterministic contract creation diff --git a/.env.anvil b/.env.anvil new file mode 100644 index 000000000..c66032f53 --- /dev/null +++ b/.env.anvil @@ -0,0 +1,2 @@ +FORK_RPC_URL="https://testnet.rpc.zora.co/" +FORK_BLOCK_NUMBER=700700 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 169f7460a..77f4ae5b6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,10 @@ lcov.info dist/ .env -.env* !.env.example package/wagmiGenerated.ts -package/chainConfigs.ts \ No newline at end of file +package/chainConfigs.ts + +# not currently using pnpm +pnpm-lock.yaml \ No newline at end of file diff --git a/package.json b/package.json index 18ad20489..45b769ad2 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,9 @@ "publish-packages": "yarn prepack && changeset publish", "storage-inspect:check": "./script/storage-check.sh check ZoraCreator1155Impl ZoraCreator1155FactoryImpl", "storage-inspect:generate": "./script/storage-check.sh generate ZoraCreator1155Impl ZoraCreator1155FactoryImpl", - "release": "yarn run prepack && changeset publish", "js-test:watch": "vitest dev", - "anvil": "anvil --fork-url https://rpc.zora.energy --fork-block-number 2550000 --chain-id 31337" + "anvil": "source .env.anvil && anvil --fork-url $FORK_RPC_URL --fork-block-number $FORK_BLOCK_NUMBER --chain-id 31337", + "release": "yarn run prepack && changeset publish" }, "files": [ "dist/", @@ -38,7 +38,8 @@ "@zoralabs/openzeppelin-contracts-upgradeable": "4.8.4", "@zoralabs/protocol-rewards": "1.1.1", "ds-test": "https://github.com/dapphub/ds-test#cd98eff28324bfac652e63a239a60632a761790b", - "forge-std": "https://github.com/foundry-rs/forge-std#cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653" + "forge-std": "https://github.com/foundry-rs/forge-std#cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653", + "solmate": "^6.1.0" }, "devDependencies": { "@changesets/cli": "^2.26.1", diff --git a/package/batchPublish.test.ts b/package/batchPublish.test.ts index 43f9d389a..0a13216ff 100644 --- a/package/batchPublish.test.ts +++ b/package/batchPublish.test.ts @@ -9,7 +9,7 @@ import { parseEther, createTestClient, } from "viem"; -import { foundry, zora } from "viem/chains"; +import { foundry, zora, zoraTestnet } from "viem/chains"; import { describe, it, expect } from "vitest"; import { zoraCreator1155FactoryImplConfig, diff --git a/package/preminter.test.ts b/package/preminter.test.ts new file mode 100644 index 000000000..4212de0aa --- /dev/null +++ b/package/preminter.test.ts @@ -0,0 +1,428 @@ +import { + createTestClient, + http, + createWalletClient, + createPublicClient, +} from "viem"; +import { foundry } from "viem/chains"; +import { describe, it, beforeEach, expect } from "vitest"; +import { parseEther } from "viem"; +import { + zoraCreator1155PreminterABI as preminterAbi, + zoraCreator1155ImplABI, +} from "./wagmiGenerated"; +import preminter from "../out/ZoraCreator1155Preminter.sol/ZoraCreator1155Preminter.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"; +import protocolRewards from "../out/ProtocolRewards.sol/ProtocolRewards.json"; +import { + ContractCreationConfig, + PremintConfig, + TokenCreationConfig, + preminterTypedDataDefinition, +} from "./preminter"; + +const walletClient = createWalletClient({ + chain: foundry, + transport: http(), +}); + +export const walletClientWithAccount = createWalletClient({ + chain: foundry, + transport: http(), +}); + +const testClient = createTestClient({ + chain: foundry, + mode: "anvil", + transport: http(), +}); + +const publicClient = createPublicClient({ + chain: foundry, + transport: http(), +}); + +type Address = `0x${string}`; + +const zeroAddress: Address = "0x0000000000000000000000000000000000000000"; + +// JSON-RPC Account +const [ + deployerAccount, + creatorAccount, + collectorAccount, + mintFeeRecipientAccount, +] = (await walletClient.getAddresses()) as [Address, Address, Address, Address]; + +type TestContext = { + preminterAddress: `0x${string}`; + anvilChainId: number; + zoraMintFee: bigint; +}; + +const deployContractAndGetAddress = async ( + args: Parameters[0] +) => { + const hash = await walletClient.deployContract(args); + return ( + await publicClient.waitForTransactionReceipt({ + hash, + }) + ).contractAddress!; +}; + +export const deployFactoryProxy = async () => { + const protocolRewardsAddress = await deployContractAndGetAddress({ + abi: protocolRewards.abi, + bytecode: protocolRewards.bytecode.object as `0x${string}`, + account: deployerAccount, + args: [], + }); + + // const mockUpgradeGateAddress = await deployContractAndGetAddress({ + // abi: mockUpgradeGate.abi, + // bytecode: mockUpgradeGate.bytecode.object as `0x${string}`, + // account: deployerAccount, + // args: [] + // }); + + const zora1155Address = await deployContractAndGetAddress({ + abi: zoraCreator1155Impl.abi, + bytecode: zoraCreator1155Impl.bytecode.object as `0x${string}`, + account: deployerAccount, + args: [0n, mintFeeRecipientAccount, zeroAddress, protocolRewardsAddress], + }); + + const fixedPriceMinterAddress = await deployContractAndGetAddress({ + abi: zoraCreatorFixedPriceSaleStrategy.abi, + bytecode: zoraCreatorFixedPriceSaleStrategy.bytecode + .object as `0x${string}`, + account: deployerAccount, + }); + + const factoryImplAddress = await deployContractAndGetAddress({ + abi: zoraCreator1155FactoryImpl.abi, + bytecode: zoraCreator1155FactoryImpl.bytecode.object as `0x${string}`, + account: deployerAccount, + args: [zora1155Address, zeroAddress, fixedPriceMinterAddress, zeroAddress], + }); + + const factoryProxyAddress = factoryImplAddress!; + + return { factoryProxyAddress, zora1155Address: zora1155Address! }; +}; + +export const deployPreminterContract = async () => { + const { factoryProxyAddress, zora1155Address } = await deployFactoryProxy(); + + const deployPreminterHash = await walletClient.deployContract({ + abi: preminter.abi, + bytecode: preminter.bytecode.object as `0x${string}`, + account: deployerAccount, + }); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash: deployPreminterHash, + }); + + 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 }; +}; + +// create token and contract creation config: +const defaultContractConfig = (): ContractCreationConfig => ({ + contractAdmin: creatorAccount, + contractURI: "ipfs://asdfasdfasdf", + contractName: "My fun NFT", +}); + +const defaultTokenConfig = (): TokenCreationConfig => ({ + tokenURI: "ipfs://tokenIpfsId0", + maxSupply: 100n, + maxTokensPerAddress: 10n, + pricePerToken: parseEther("0.1"), + mintStart: 0n, + mintDuration: 100n, + royaltyMintSchedule: 30, + royaltyBPS: 200, + royaltyRecipient: creatorAccount, +}); + +const defaultPremintConfig = (): PremintConfig => ({ + contractConfig: defaultContractConfig(), + tokenConfig: defaultTokenConfig(), + deleted: false, + uid: 105, + version: 0, +}); + +// const useForkContract = true; + +describe("ZoraCreator1155Preminter", () => { + beforeEach(async (ctx) => { + // deploy signature minter contract + await testClient.setBalance({ + address: deployerAccount, + value: parseEther("10"), + }); + + // ctx.forkedChainId = zoraTestnet.id; + ctx.anvilChainId = foundry.id; + + let preminterAddress: Address; + let zora1155Address: Address; + + const deployed = await deployPreminterContract(); + + preminterAddress = deployed.preminterAddress; + zora1155Address = deployed.zora1155Address; + + ctx.zoraMintFee = await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: zora1155Address, + functionName: "mintFee", + }); + + 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(); + + const signedMessage = await walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: preminterAddress, + chainId: 999, + premintConfig, + }), + account: creatorAccount, + }); + + console.log({ + creatorAccount, + signedMessage, + premintConfig, + contractAddress: await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getContractAddress", + args: [defaultContractConfig()], + }), + }); + }, + 20 * 1000 + ); + it( + "can sign and recover a signature", + async ({ preminterAddress: preminterAddress, anvilChainId }) => { + const premintConfig = defaultPremintConfig(); + + console.log({ + defaultMind: defaultPremintConfig(), + }); + + // sign message containing contract and token creation config and uid + const signedMessage = await walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: preminterAddress, + // we need to sign here for the anvil chain, cause thats where it is run on + chainId: anvilChainId, + premintConfig, + }), + account: creatorAccount, + }); + + // recover and verify address is correct + const recoveredAddress = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "recoverSigner", + args: [premintConfig, signedMessage], + }); + + expect(recoveredAddress).to.equal(creatorAccount); + }, + + 20 * 1000 + ); + + it( + "can sign and mint multiple tokens", + async ({ + zoraMintFee, + anvilChainId, + preminterAddress: preminterAddress, + }) => { + // setup contract and token creation parameters + const premintConfig = defaultPremintConfig(); + + // lets make it a random number to not break the existing tests that expect fresh data + premintConfig.uid = Math.round(Math.random() * 1000000); + + let contractAddress = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getContractAddress", + args: [premintConfig.contractConfig], + }); + + // have creator sign the message to create the contract + // and the token + const signedMessage = await walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: preminterAddress, + chainId: anvilChainId, + premintConfig, + }), + // signer account is the creator + account: creatorAccount, + }); + + const quantityToMint = 2n; + + const valueToSend = + (zoraMintFee + premintConfig.tokenConfig.pricePerToken) * + quantityToMint; + + const comment = "I love this!"; + + await testClient.setBalance({ + address: collectorAccount, + value: 10n * 10n ** 18n, + }); + + // get the premint status - it should not be minted + let tokenId = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getPremintedTokenId", + args: [premintConfig.contractConfig, premintConfig.uid], + }); + + expect(tokenId).toBe(0n); + // expect(contractAddress).toBe(zeroAddress); + + // now have the collector execute the first signed message; + // it should create the contract, the token, + // and min the quantity to mint tokens to the collector + // the signature along with contract + token creation + // parameters are required to call this function + const mintHash = await walletClient.writeContract({ + abi: preminterAbi, + functionName: "premint", + account: collectorAccount, + address: preminterAddress, + args: [premintConfig, signedMessage, quantityToMint, comment], + value: valueToSend, + }); + + // ensure it succeeded + 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({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getPremintedTokenId", + args: [premintConfig.contractConfig, premintConfig.uid], + }); + + expect(newTokenId).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], + }); + + // get token balance - should be amount that was created + expect(tokenBalance).toBe(quantityToMint); + + const premintConfig2 = { + ...premintConfig, + uid: premintConfig.uid + 1, + tokenConfig: { + ...premintConfig.tokenConfig, + tokenURI: "ipfs://tokenIpfsId2", + pricePerToken: parseEther("0.05"), + }, + }; + + // sign the message to create the second token + const signedMessage2 = await walletClient.signTypedData({ + ...preminterTypedDataDefinition({ + verifyingContract: preminterAddress, + chainId: foundry.id, + premintConfig: premintConfig2, + }), + account: creatorAccount, + }); + + const quantityToMint2 = 4n; + + const valueToSend2 = + (zoraMintFee + premintConfig2.tokenConfig.pricePerToken) * + quantityToMint2; + + // now have the collector execute the second signed message. + // it should create a new token against the existing contract + const mintHash2 = await walletClient.writeContract({ + abi: preminterAbi, + functionName: "premint", + account: collectorAccount, + address: preminterAddress, + args: [premintConfig2, signedMessage2, quantityToMint2, comment], + value: valueToSend2, + }); + + expect( + (await publicClient.waitForTransactionReceipt({ hash: mintHash2 })) + .status + ).toBe("success"); + + // now premint status for the second mint, it should be minted + tokenId = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "getPremintedTokenId", + args: [premintConfig2.contractConfig, premintConfig2.uid], + }); + + expect(tokenId).not.toBe(0n); + + // get balance of second token + const tokenBalance2 = await publicClient.readContract({ + abi: zoraCreator1155ImplABI, + address: contractAddress, + functionName: "balanceOf", + args: [collectorAccount, tokenId], + }); + + expect(tokenBalance2).toBe(quantityToMint2); + }, + // 10 second timeout + 40 * 1000 + ); +}); diff --git a/package/preminter.ts b/package/preminter.ts new file mode 100644 index 000000000..d4a35255f --- /dev/null +++ b/package/preminter.ts @@ -0,0 +1,77 @@ +import { Address } from "abitype"; +import { ExtractAbiFunction, AbiParametersToPrimitiveTypes } from "abitype"; +import { zoraCreator1155PreminterABI as preminterAbi } from "./wagmiGenerated"; +import { TypedDataDefinition } from "viem"; + +type PreminterHashInputs = ExtractAbiFunction< + typeof preminterAbi, + "premintHashData" +>["inputs"]; + +type PreminterHashDataTypes = + AbiParametersToPrimitiveTypes; + +export type PremintConfig = PreminterHashDataTypes[0]; +export type ContractCreationConfig = PremintConfig["contractConfig"]; +export type TokenCreationConfig = PremintConfig["tokenConfig"]; + +// Convenience method to create the structured typed data +// needed to sign for a premint contract and token +export const preminterTypedDataDefinition = ({ + verifyingContract, + premintConfig, + chainId, +}: { + verifyingContract: Address; + premintConfig: PremintConfig; + chainId: number; +}) => { + const { contractConfig, tokenConfig, uid, version, deleted } = premintConfig; + const types = { + Premint: [ + { name: "contractConfig", type: "ContractCreationConfig" }, + { name: "tokenConfig", type: "TokenCreationConfig" }, + { name: "uid", type: "uint32" }, + { name: "version", type: "uint32" }, + { 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" }, + { name: "maxTokensPerAddress", type: "uint64" }, + { name: "pricePerToken", type: "uint96" }, + { name: "mintStart", type: "uint64" }, + { name: "mintDuration", type: "uint64" }, + { name: "royaltyMintSchedule", type: "uint32" }, + { name: "royaltyBPS", type: "uint32" }, + { name: "royaltyRecipient", type: "address" }, + ], + }; + + const result: TypedDataDefinition = { + domain: { + chainId, + name: "Preminter", + version: "0.0.1", + verifyingContract: verifyingContract, + }, + types, + message: { + contractConfig, + tokenConfig, + uid, + version, + deleted, + }, + primaryType: "Premint", + }; + + // console.log({ result, deleted }); + + return result; +}; diff --git a/remappings.txt b/remappings.txt index 740c53f29..76854be3f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,5 @@ forge-std/=node_modules/forge-std/src/ @zoralabs/openzeppelin-contracts-upgradeable/=node_modules/@zoralabs/openzeppelin-contracts-upgradeable/ @zoralabs/protocol-rewards/src/=node_modules/@zoralabs/protocol-rewards/src/ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -_imagine=_imagine/ \ No newline at end of file +_imagine=_imagine/ +solemate/=/node_modules/solemate/src/ \ No newline at end of file diff --git a/script/DeployPreminter.s.sol b/script/DeployPreminter.s.sol new file mode 100644 index 000000000..cc9b97d47 --- /dev/null +++ b/script/DeployPreminter.s.sol @@ -0,0 +1,42 @@ +// 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 DeployPreminter is ZoraDeployerBase { + function run() public returns (string memory) { + Deployment memory deployment = getDeployment(); + + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + ZoraCreator1155FactoryImpl factory = ZoraCreator1155FactoryImpl(deployment.factoryProxy); + + vm.startBroadcast(deployerPrivateKey); + + ZoraCreator1155Preminter preminter = new ZoraCreator1155Preminter(); + preminter.initialize(factory); + + vm.stopBroadcast(); + + deployment.preminter = address(preminter); + + return getDeploymentJSON(deployment); + } +} diff --git a/script/EstimatePreminterGas.s.sol b/script/EstimatePreminterGas.s.sol new file mode 100644 index 000000000..e95737921 --- /dev/null +++ b/script/EstimatePreminterGas.s.sol @@ -0,0 +1,90 @@ +// 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/script/TestCreateDeterministic.sol b/script/TestCreateDeterministic.sol new file mode 100644 index 000000000..28ae44a4d --- /dev/null +++ b/script/TestCreateDeterministic.sol @@ -0,0 +1,68 @@ +// 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 {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.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"; + +contract DeployScript is ZoraDeployerBase { + function run() public { + // ChainConfig memory chainConfig = getChainConfig(); + + // console2.log("zoraFeeAmount", chainConfig.mintFeeAmount); + // console2.log("zoraFeeRecipient", chainConfig.mintFeeRecipient); + // console2.log("factoryOwner", chainConfig.factoryOwner); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ + royaltyBPS: 10, + royaltyRecipient: vm.addr(5), + royaltyMintSchedule: 100 + }); + bytes[] memory initSetup = new bytes[](1); + initSetup[0] = abi.encodeWithSelector(IZoraCreator1155.setupNewToken.selector, "ipfs://asdfadsf", 100); + string memory uri = "ipfs://asdfadsf"; + string memory nameA = "nameA"; + + vm.startBroadcast(deployerPrivateKey); + + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(0, address(0), address(0), address(new ProtocolRewards())); + // get above constructor args encoded for verification later: + ZoraCreator1155FactoryImpl factory = new ZoraCreator1155FactoryImpl( + zoraCreator1155Impl, + IMinter1155(address(1)), + IMinter1155(address(2)), + IMinter1155(address(3)) + ); + + address factoryOwner = deployer; + + // 1. create the proxy, pointing it to the factory implentation and setting the owner + ZoraCreator1155FactoryImpl proxy = ZoraCreator1155FactoryImpl( + payable(new Zora1155Factory(address(factory), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, factoryOwner))) + ); + + address createdErc1155 = proxy.createContractDeterministic(uri, nameA, royaltyConfig, payable(deployer), initSetup); + + console.log("deployed erc1155 at", createdErc1155); + console.log("constructor args", string(abi.encode(0, address(0), address(0)))); + vm.stopBroadcast(); + } +} diff --git a/script/ZoraDeployerBase.sol b/script/ZoraDeployerBase.sol index 138cf25b6..281d5abf8 100644 --- a/script/ZoraDeployerBase.sol +++ b/script/ZoraDeployerBase.sol @@ -26,6 +26,7 @@ abstract contract ZoraDeployerBase is ScriptDeploymentConfig, Script { vm.serializeAddress(deploymentJsonKey, REDEEM_MINTER_FACTORY, deployment.redeemMinterFactory); vm.serializeAddress(deploymentJsonKey, CONTRACT_1155_IMPL, deployment.contract1155Impl); vm.serializeAddress(deploymentJsonKey, FACTORY_IMPL, deployment.factoryImpl); + vm.serializeAddress(deploymentJsonKey, PREMINTER, deployment.preminter); deploymentJson = vm.serializeAddress(deploymentJsonKey, FACTORY_PROXY, deployment.factoryProxy); console2.log(deploymentJson); } diff --git a/script/copy-deployed-contracts.mjs b/script/copy-deployed-contracts.mjs index 8aed2c7d1..af1baf968 100644 --- a/script/copy-deployed-contracts.mjs +++ b/script/copy-deployed-contracts.mjs @@ -2,11 +2,9 @@ import { writeFile, readFile } from "fs/promises"; import esMain from "es-main"; import { glob } from "glob"; -async function copyEnvironmentRunFiles(isDeploy) { +async function copyEnvironmentRunFiles(scriptName) { const latestFiles = await glob( - isDeploy - ? "broadcast/Deploy.s.sol/*/run-latest.json" - : "broadcast/Upgrade.s.sol/*/run-latest.json" + `broadcast/${scriptName}/*/run-latest.json` ); for (const file of latestFiles) { @@ -41,9 +39,13 @@ async function copyEnvironmentRunFiles(isDeploy) { if (esMain(import.meta)) { const command = process.argv[2]; - let deploy = false; - if (command === "deploy") { - deploy = true; + let scriptName = 'Deploy.s.sol'; + + if (command === "upgrade"){ + scriptName = 'Upgrade.s.sol'; + } else if (command === 'deploy-premint') { + scriptName = 'DeployPreminter.s.sol' } - await copyEnvironmentRunFiles(deploy); + + await copyEnvironmentRunFiles(scriptName); } diff --git a/src/deployment/DeploymentConfig.sol b/src/deployment/DeploymentConfig.sol index df5856c3e..c43a12077 100644 --- a/src/deployment/DeploymentConfig.sol +++ b/src/deployment/DeploymentConfig.sol @@ -31,6 +31,8 @@ struct Deployment { address factoryImpl; /// @notice Factory proxy contract that creates zora drops style NFT contracts address factoryProxy; + /// @notice Preminter contract address + address preminter; } abstract contract DeploymentConfig is CommonBase { @@ -55,6 +57,7 @@ abstract contract DeploymentConfig is CommonBase { string constant CONTRACT_1155_IMPL = "CONTRACT_1155_IMPL"; string constant FACTORY_IMPL = "FACTORY_IMPL"; string constant FACTORY_PROXY = "FACTORY_PROXY"; + string constant PREMINTER = "PREMINTER"; /// @notice Return a prefixed key for reading with a ".". /// @param key key to prefix @@ -83,6 +86,7 @@ abstract contract DeploymentConfig is CommonBase { deployment.contract1155Impl = json.readAddress(getKeyPrefix(CONTRACT_1155_IMPL)); deployment.factoryImpl = json.readAddress(getKeyPrefix(FACTORY_IMPL)); deployment.factoryProxy = json.readAddress(getKeyPrefix(FACTORY_PROXY)); + deployment.preminter = json.readAddress(getKeyPrefix(PREMINTER)); } } diff --git a/src/factory/ZoraCreator1155FactoryImpl.sol b/src/factory/ZoraCreator1155FactoryImpl.sol index ba1bdd552..1f48a75b0 100644 --- a/src/factory/ZoraCreator1155FactoryImpl.sol +++ b/src/factory/ZoraCreator1155FactoryImpl.sol @@ -12,6 +12,8 @@ import {IContractMetadata} from "../interfaces/IContractMetadata.sol"; import {Ownable2StepUpgradeable} from "../utils/ownable/Ownable2StepUpgradeable.sol"; import {FactoryManagedUpgradeGate} from "../upgrades/FactoryManagedUpgradeGate.sol"; import {Zora1155} from "../proxies/Zora1155.sol"; +import {Create2Upgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/Create2Upgradeable.sol"; +import {CREATE3} from "solmate/src/utils/CREATE3.sol"; import {ContractVersionBase} from "../version/ContractVersionBase.sol"; @@ -66,14 +68,56 @@ contract ZoraCreator1155FactoryImpl is IZoraCreator1155Factory, ContractVersionB /// @param defaultAdmin The default admin for the contract /// @param setupActions The actions to perform on the new contract upon initialization function createContract( - string memory newContractURI, + string calldata newContractURI, string calldata name, ICreatorRoyaltiesControl.RoyaltyConfiguration memory defaultRoyaltyConfiguration, address payable defaultAdmin, bytes[] calldata setupActions ) external returns (address) { - address newContract = address(new Zora1155(address(implementation))); + Zora1155 newContract = new Zora1155(address(implementation)); + _initializeContract(Zora1155(newContract), newContractURI, name, defaultRoyaltyConfiguration, defaultAdmin, setupActions); + + return address(newContract); + } + + function createContractDeterministic( + string calldata newContractURI, + string calldata name, + ICreatorRoyaltiesControl.RoyaltyConfiguration calldata defaultRoyaltyConfiguration, + address payable defaultAdmin, + bytes[] calldata setupActions + ) external returns (address) { + bytes32 digest = _hashContract(msg.sender, newContractURI, name, defaultAdmin); + + address createdContract = CREATE3.deploy(digest, abi.encodePacked(type(Zora1155).creationCode, abi.encode(implementation)), 0); + + Zora1155 newContract = Zora1155(payable(createdContract)); + + _initializeContract(newContract, newContractURI, name, defaultRoyaltyConfiguration, defaultAdmin, setupActions); + + return address(newContract); + } + + function deterministicContractAddress( + address msgSender, + string calldata newContractURI, + string calldata name, + address contractAdmin + ) external view returns (address) { + bytes32 digest = _hashContract(msgSender, newContractURI, name, contractAdmin); + + return CREATE3.getDeployed(digest); + } + + function _initializeContract( + Zora1155 newContract, + string calldata newContractURI, + string calldata name, + ICreatorRoyaltiesControl.RoyaltyConfiguration memory defaultRoyaltyConfiguration, + address payable defaultAdmin, + bytes[] calldata setupActions + ) private { emit SetupNewContract({ newContract: address(newContract), creator: msg.sender, @@ -83,9 +127,15 @@ contract ZoraCreator1155FactoryImpl is IZoraCreator1155Factory, ContractVersionB defaultRoyaltyConfiguration: defaultRoyaltyConfiguration }); - IZoraCreator1155Initializer(newContract).initialize(name, newContractURI, defaultRoyaltyConfiguration, defaultAdmin, setupActions); + IZoraCreator1155Initializer(address(newContract)).initialize(name, newContractURI, defaultRoyaltyConfiguration, defaultAdmin, setupActions); + } + + function _hashContract(address msgSender, string calldata newContractURI, string calldata name, address contractAdmin) private pure returns (bytes32) { + return keccak256(abi.encode(msgSender, contractAdmin, _stringHash(newContractURI), _stringHash(name))); + } - return address(newContract); + function _stringHash(string calldata value) private pure returns (bytes32) { + return keccak256(bytes(value)); } /// /// diff --git a/src/interfaces/IZoraCreator1155Factory.sol b/src/interfaces/IZoraCreator1155Factory.sol index 19cdfa5f5..b72a2b469 100644 --- a/src/interfaces/IZoraCreator1155Factory.sol +++ b/src/interfaces/IZoraCreator1155Factory.sol @@ -29,6 +29,22 @@ interface IZoraCreator1155Factory is IVersionedContract { bytes[] calldata setupActions ) external returns (address); + /// @notice creates the contract, using a deterministic address based on the name, contract uri, and defaultAdmin + function createContractDeterministic( + string calldata contractURI, + string calldata name, + ICreatorRoyaltiesControl.RoyaltyConfiguration calldata defaultRoyaltyConfiguration, + address payable defaultAdmin, + bytes[] calldata setupActions + ) external returns (address); + + function deterministicContractAddress( + address msgSender, + string calldata newContractURI, + string calldata name, + address contractAdmin + ) external view returns (address); + function defaultMinters() external returns (IMinter1155[] memory minters); function initialize(address _owner) external; diff --git a/src/premint/EIP712UpgradeableWithChainId.sol b/src/premint/EIP712UpgradeableWithChainId.sol new file mode 100644 index 000000000..067a663f7 --- /dev/null +++ b/src/premint/EIP712UpgradeableWithChainId.sol @@ -0,0 +1,106 @@ +// 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/README.md b/src/premint/README.md new file mode 100644 index 000000000..6b65baff7 --- /dev/null +++ b/src/premint/README.md @@ -0,0 +1,59 @@ +# Preminter + +## Design + +A Preminter contract validates signatures and executes actions to 1. deploy contracts, 2. create tokens, 3. setup created token parameters +4. mint tokens to the executor of the transaction as a reward. + +## Design + +- General goal: Create a contract (”SignedExecutor”) that validates signatures and executes actions to 1. deploy contracts, 2. create tokens, 3. setup created token parameters 4. mint tokens to the executor of the transaction as a reward +- A creator can create multiple tokens without needing to pay any gas. Each token creation intent is bundled into a signature, which can be executed later by any account. The signature for each token are unordered; they can be executed in any order, and the order they are executed on will determine their token id. +## Contracts + +`Preminter`: Executes commands on the 1155 contract factory, and created 1155 contracts + +Constraints: + * **Contract creation params must be unique** - the combination of creator + metadata uri + name must be unique. The Preminter can only create a single contract for each combination of creator, metadat uri, and name. There must be some sort of validation in the create flow that ensures a contract has not been created with those parameters. + * **For each contract, token parameters must be unique.** The combination of parameters for the token to be created, including metadata uri, max supply, duration, etc **must be unique within each contract.** i.e. a contract cannot have two tokens with the same parameters. This is because we use this combination to ensure that a signature to create the token can only be executed once. An alternative design is to require a unique nonce to be appended to the parameters, which would ensure uniqueness; this would need to be provided by the backend. + +Functions: + * `premint`: takes an [EIP712 signature](https://eips.ethereum.org/EIPS/eip-712) created by a creator, contract and token creation params, and creates a contract if the contract doesn’t exist and creates a new token, or creates a new token on an existing contract if it exists. It then mints a specified quantity of tokens to the executor as a reward. These parameters are the same both if a new contract is created or a token is created on an existing contract. The signature must have been previously created from a hash built from all of the input parameters; the hash can be generated using `premintHashData`. **Each signature can only be executed against once**; this is enforced through uniqueness of the contract creation params, the token creation params, and quantity to mint. + * inputs: + * `contractCreationConfig` + * `contractAdmin` - creator/admin of the contract. **Must match the address of the account that signed the signature** + * `contractURI` - metadata uri of the contract + * `defaultRoyaltyConfiguration` - contract royalty config + * `tokenCreationConfig` + * `tokenURI` - metadata uri of the token to be created + * `tokenMaxSupply` - max supply of the token to be created + * `saleDuration` - how long this token should be on sale for, from the time of the first mint. If 0, duration is infinite + * `maxTokensPerAddress` - max tokens an address can mint + * `pricePerToken` - cost to mint each token + * `uid` - unique id of the token scoped within the contract. Ensures that multiple signatures for a token cannot be executed thus creating two tokens. + * `signature` - signature signed message containing all of the above parameters + * `quantityToMint` - how many of the initial tokens to mint to the executor + +## Functional flow: + +### Diagrams + +Creating a new contract + token: + +![Preminter creation flow](../../uml/generated/gasslessCreate-creation-sequence.svg) +![Preminter creation flow](../../uml/generated/gasslessCreate-creation-activity.svg) + +Collecting: + +![Preminter collection flow](../../uml/generated/gasslessCreate-collecting-sequence.svg) +![Preminter collection flow](../../uml/generated/gasslessCreate-collecting-activity.svg) + +* In the front-end a creator creates a signature for contract and token creation. The signature is created off-chain by the creator's account on a hash of the above said parameters. It there are additional tokens to be created, signatures are created for each token to be created. There must be some validation that a signature with the same parameters has not already been created (see constraints above). This can be done by checking against the uniqueness of the created signature. +* Once the creator has signed the message, a backend service (another db or blockchain) must store these signatures which can be retreived later by a collector. This backend must store both the contract + token creation parameters and the signature. +* A collector lands on a page that loads the signature and contract creation params based on the bytes32 signature. The contract + token creation parameters and signature are loaded from the backend service or a subgraph which loads the previously stored signature. +* The collector account executs the function `premint`, passing the corresponding signature and contract creation params. If the contract has not been created, it is created. A new token is created on that contract, and `quantityToMint` tokens are minted to the executor. + +## Additional caveats + +* The `Preminter` contract is granted the role `PERMISSION_BIT_MINTER` on the 1155 contract, allowing it to create new tokens. +* There are some issues where marketplaces show tx.origin of a transaction as the contract creator, which in this case would show the collector as the contract creator. \ No newline at end of file diff --git a/src/premint/ZoraCreator1155Preminter.sol b/src/premint/ZoraCreator1155Preminter.sol new file mode 100644 index 000000000..3f27818e6 --- /dev/null +++ b/src/premint/ZoraCreator1155Preminter.sol @@ -0,0 +1,366 @@ +// 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/test/factory/ZoraCreator1155Factory.t.sol b/test/factory/ZoraCreator1155Factory.t.sol index aea59306b..bcaa7a43b 100644 --- a/test/factory/ZoraCreator1155Factory.t.sol +++ b/test/factory/ZoraCreator1155Factory.t.sol @@ -10,6 +10,7 @@ import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Fact import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; +import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {MockContractMetadata} from "../mock/MockContractMetadata.sol"; contract ZoraCreator1155FactoryTest is Test { @@ -124,4 +125,125 @@ contract ZoraCreator1155FactoryTest is Test { vm.expectRevert(abi.encodeWithSignature("UpgradeToMismatchedContractName(string,string)", "ZORA 1155 Contract Factory", "name")); proxy.upgradeTo(address(mockContractMetadata)); } + + function test_createContractDeterminisitc_createsContractAtSameAddressForNameAndUri( + string calldata nameA, + string calldata uri, + address contractAdmin, + // this number will determine how transactions the factory makes before + // creating the deterministic contract. it should not affect the address + uint16 numberOfCallsBeforeCreation + ) external { + vm.assume(contractAdmin != address(0)); + + address contractCreator = vm.addr(1); + + // we can know ahead of time the expected address + address expectedContractAddress = factory.deterministicContractAddress(contractCreator, uri, nameA, contractAdmin); + + // create parameters for contract creation + ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ + royaltyBPS: 10, + royaltyRecipient: vm.addr(5), + royaltyMintSchedule: 100 + }); + bytes[] memory initSetup = new bytes[](1); + initSetup[0] = abi.encodeWithSelector(IZoraCreator1155.setupNewToken.selector, "ipfs://asdfadsf", 100); + + // create x number of contracts via the factory, this should affect the nonce. + for (uint256 i = 0; i < numberOfCallsBeforeCreation; i++) { + factory.createContract("ipfs://someOtherUri", "someOtherName", royaltyConfig, payable(vm.addr(3)), initSetup); + } + + // now create deterministically, address should match expected address + vm.prank(contractCreator); + address createdAddress = factory.createContractDeterministic(uri, nameA, royaltyConfig, payable(contractAdmin), new bytes[](0)); + + assertEq(createdAddress, expectedContractAddress); + } + + function test_createContractDeterministic_whenContractUpgraded_stillHasSameAddress() external { + string memory uri = "ipfs://asdfadsf"; + string memory nameA = "nameA"; + address contractAdmin = vm.addr(1); + + address factoryOwner = vm.addr(10); + + // account that creates the contract (not necessarily the owner/admin) + address contractCreator = vm.addr(2); + + // 1. create the proxy, pointing it to the factory implentation and setting the owner + ZoraCreator1155FactoryImpl proxy = ZoraCreator1155FactoryImpl( + payable(new Zora1155Factory(address(factory), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, factoryOwner))) + ); + + // get the deterministic address of the contract before its created + address expectedContractAddress = proxy.deterministicContractAddress(contractCreator, uri, nameA, contractAdmin); + + uint256 newMintFeeAmount = 1 ether; + + // 2. update the erc1155 implementation: + // * create a new version of the erc1155 implementation + // * create a new factory that points to that new erc1155 implementation, + // * upgrade the proxy to point to the new factory + IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, address(0), address(0), address(new ProtocolRewards())); + + ZoraCreator1155FactoryImpl newFactoryImpl = new ZoraCreator1155FactoryImpl( + newZoraCreator, + IMinter1155(address(0)), + IMinter1155(address(0)), + IMinter1155(address(0)) + ); + + vm.prank(factoryOwner); + proxy.upgradeTo(address(newFactoryImpl)); + + // sanity check - make sure that the proxy erc1155 implementation is pointing to the new implementation + assertEq(address(proxy.implementation()), address(newZoraCreator)); + + // 3. Create a contract with a deterministic address, it should match the address from before the upgrade + ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ + royaltyBPS: 10, + royaltyRecipient: vm.addr(5), + royaltyMintSchedule: 100 + }); + bytes[] memory initSetup = new bytes[](1); + initSetup[0] = abi.encodeWithSelector(IZoraCreator1155.setupNewToken.selector, "ipfs://asdfadsf", 100); + + // now create deterministically, address should match expected address + vm.prank(contractCreator); + address createdAddress = proxy.createContractDeterministic(uri, nameA, royaltyConfig, payable(contractAdmin), initSetup); + + assertEq(createdAddress, expectedContractAddress); + } + + function test_createContractDeterministic_createdContractcontractCanBeUpgraded() external { + string memory uri = "ipfs://asdfadsf"; + string memory nameA = "nameA"; + address contractAdmin = vm.addr(1); + + // 1. Have the factory the contract deterministically + ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ + royaltyBPS: 10, + royaltyRecipient: vm.addr(5), + royaltyMintSchedule: 100 + }); + bytes[] memory initSetup = new bytes[](1); + initSetup[0] = abi.encodeWithSelector(IZoraCreator1155.setupNewToken.selector, "ipfs://asdfadsf", 100); + + // now create deterministically, address should match expected address + address createdAddress = factory.createContractDeterministic(uri, nameA, royaltyConfig, payable(contractAdmin), initSetup); + + ZoraCreator1155Impl creatorProxy = ZoraCreator1155Impl(createdAddress); + + // 2. upgrade the created contract by creating a new contract and upgrading the existing one to point to it. + uint256 newMintFeeAmount = 1 ether; + IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, address(0), address(0), address(new ProtocolRewards())); + + vm.prank(creatorProxy.owner()); + creatorProxy.upgradeTo(address(newZoraCreator)); + + // 3. check that proxy contract was upgraded + assertEq(creatorProxy.mintFee(), newMintFeeAmount); + } } diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol new file mode 100644 index 000000000..2b6291846 --- /dev/null +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; +import {ECDSAUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; + +import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {Zora1155} from "../../src/proxies/Zora1155.sol"; +import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; +import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; +import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; +import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Factory.sol"; +import {ILimitedMintPerAddress} from "../../src/interfaces/ILimitedMintPerAddress.sol"; +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 {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; + +contract ZoraCreator1155PreminterTest is Test { + ZoraCreator1155Preminter internal preminter; + ZoraCreator1155FactoryImpl internal factory; + // setup contract config + uint256 creatorPrivateKey = 0xA11CE; + address creator; + + ICreatorRoyaltiesControl.RoyaltyConfiguration defaultRoyaltyConfig; + + event Preminted( + address indexed contractAddress, + uint256 indexed tokenId, + bool indexed createdNewContract, + uint32 uid, + ZoraCreator1155Preminter.ContractCreationConfig contractConfig, + ZoraCreator1155Preminter.TokenCreationConfig tokenConfig, + address minter, + uint256 quantityMinted + ); + + function setUp() external { + ProtocolRewards rewards = new ProtocolRewards(); + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(0, makeAddr("zora"), address(0), address(rewards)); + ZoraCreatorFixedPriceSaleStrategy fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); + factory = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); + uint32 royaltyBPS = 2; + uint32 royaltyMintSchedule = 20; + address royaltyRecipient = vm.addr(4); + + defaultRoyaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ + royaltyBPS: royaltyBPS, + royaltyRecipient: royaltyRecipient, + royaltyMintSchedule: royaltyMintSchedule + }); + + preminter = new ZoraCreator1155Preminter(); + preminter.initialize(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 makeDefaultTokenCreationConfig() internal view returns (ZoraCreator1155Preminter.TokenCreationConfig memory) { + return + ZoraCreator1155Preminter.TokenCreationConfig({ + tokenURI: "blah.token", + maxSupply: 10, + maxTokensPerAddress: 5, + pricePerToken: 0, + mintStart: 0, + mintDuration: 0, + royaltyMintSchedule: defaultRoyaltyConfig.royaltyMintSchedule, + royaltyBPS: defaultRoyaltyConfig.royaltyBPS, + royaltyRecipient: defaultRoyaltyConfig.royaltyRecipient + }); + } + + function makeDefaultPremintConfig() internal view returns (ZoraCreator1155Preminter.PremintConfig memory) { + return + ZoraCreator1155Preminter.PremintConfig({ + contractConfig: makeDefaultContractCreationConfig(), + 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(); + + // how many tokens are minted to the executor + uint256 quantityToMint = 4; + uint256 chainId = block.chainid; + string memory comment = "hi"; + + // 2. Call smart contract to get digest to sign for creation params. + bytes32 digest = preminter.premintHashData(premintConfig, address(preminter), 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); + + // 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); + + // 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); + + // alter the token creation config, create a new signature with the existing + // contract config and new token config + premintConfig.tokenConfig.tokenURI = "blah2.token"; + premintConfig.uid++; + + digest = preminter.premintHashData(premintConfig, address(preminter), chainId); + signature = _sign(creatorPrivateKey, digest); + + // premint with new token config and signature + vm.prank(premintExecutor); + (, tokenId) = preminter.premint(premintConfig, signature, quantityToMint, comment); + + // a new token shoudl have been created, with x tokens minted to the executor, on the same contract address + // as before since the contract config didnt change + assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), quantityToMint); + } + + function test_signatureForSameContractandUid_cannotBeExecutedTwice() external { + // 1. Make contract creation params + + // configuration of contract to create + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // how many tokens are minted to the executor + uint256 quantityToMint = 4; + uint256 chainId = block.chainid; + address premintExecutor = vm.addr(701); + string memory comment = "I love it"; + + _signAndExecutePremint(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); + + 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); + + // change the version, it should still revert + premintConfig.version++; + signature = _signPremint(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); + + // change the uid, it should not revert + premintConfig.uid++; + signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + + preminter.premint(premintConfig, signature, quantityToMint, comment); + } + + function test_deleted_preventsTokenFromBeingMinted() external { + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + premintConfig.deleted = true; + uint chainId = block.chainid; + address premintExecutor = vm.addr(701); + uint256 quantityToMint = 2; + string memory comment = "I love it"; + + // 2. Call smart contract to get digest to sign for creation params. + (address contractAddress, uint256 tokenId) = _signAndExecutePremint( + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); + + assertEq(contractAddress, address(0)); + assertEq(tokenId, 0); + + // make sure no contract was created + assertEq(preminter.getContractAddress(premintConfig.contractConfig).code.length, 0); + } + + function test_emitsPremint_whenNewContract() external { + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // 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); + + // this account will be used to execute the premint, and should result in a contract being created + address premintExecutor = vm.addr(701); + + string memory comment = "I love it"; + + 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, + createdNewContract, + premintConfig.uid, + premintConfig.contractConfig, + premintConfig.tokenConfig, + premintExecutor, + quantityToMint + ); + preminter.premint(premintConfig, signature, quantityToMint, comment); + } + + function test_onlyOwner_hasAdminRights_onCreatedToken() public { + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // how many tokens are minted to the executor + uint256 quantityToMint = 4; + uint256 chainId = block.chainid; + // this account will be used to execute the premint, and should result in a contract being created + address premintExecutor = vm.addr(701); + string memory comment = "I love it"; + + (address createdContractAddress, uint256 newTokenId) = _signAndExecutePremint( + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); + + // get the contract address from the preminter based on the contract hash id. + IZoraCreator1155 created1155Contract = IZoraCreator1155(createdContractAddress); + + ZoraCreatorFixedPriceSaleStrategy.SalesConfig memory newSalesConfig = ZoraCreatorFixedPriceSaleStrategy.SalesConfig({ + pricePerToken: 5 ether, + saleStart: 0, + saleEnd: 0, + maxTokensPerAddress: 5, + fundsRecipient: creator + }); + + IMinter1155 fixedPrice = factory.fixedPriceMinter(); + + // have the premint contract try to set the sales config - it should revert with + // the expected UserMissingRole error + vm.expectRevert( + abi.encodeWithSelector( + IZoraCreator1155.UserMissingRoleForToken.selector, + address(preminter), + newTokenId, + ZoraCreator1155Impl(address(created1155Contract)).PERMISSION_BIT_SALES() + ) + ); + vm.prank(address(preminter)); + created1155Contract.callSale( + newTokenId, + fixedPrice, + abi.encodeWithSelector(ZoraCreatorFixedPriceSaleStrategy.setSale.selector, newTokenId, newSalesConfig) + ); + + // have admin/creator try to set the sales config - it should succeed + vm.prank(creator); + created1155Contract.callSale( + newTokenId, + fixedPrice, + abi.encodeWithSelector(ZoraCreatorFixedPriceSaleStrategy.setSale.selector, newTokenId, newSalesConfig) + ); + + // have the premint contract try to set royalties config - it should revert + vm.expectRevert( + abi.encodeWithSelector( + IZoraCreator1155.UserMissingRoleForToken.selector, + address(preminter), + newTokenId, + ZoraCreator1155Impl(address(created1155Contract)).PERMISSION_BIT_FUNDS_MANAGER() + ) + ); + vm.prank(address(preminter)); + created1155Contract.updateRoyaltiesForToken(newTokenId, defaultRoyaltyConfig); + + // have admin/creator try to set royalties config - it should succeed + vm.prank(creator); + created1155Contract.updateRoyaltiesForToken(newTokenId, defaultRoyaltyConfig); + } + + function test_premintStatus_getsStatus() external { + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // how many tokens are minted to the executor + uint256 quantityToMint = 4; + uint256 chainId = block.chainid; + // this account will be used to execute the premint, and should result in a contract being created + address premintExecutor = vm.addr(701); + string memory comment = "I love it"; + + uint32 firstUid = premintConfig.uid; + uint32 secondUid = firstUid + 1; + + ZoraCreator1155Preminter.ContractCreationConfig memory firstContractConfig = premintConfig.contractConfig; + ZoraCreator1155Preminter.ContractCreationConfig memory secondContractConfig = ZoraCreator1155Preminter.ContractCreationConfig( + firstContractConfig.contractAdmin, + firstContractConfig.contractURI, + string.concat(firstContractConfig.contractName, "4") + ); + + (address resultContractAddress, uint256 newTokenId) = _signAndExecutePremint( + premintConfig, + creatorPrivateKey, + chainId, + premintExecutor, + quantityToMint, + comment + ); + address contractAddress = preminter.getContractAddress(firstContractConfig); + uint256 tokenId = preminter.getPremintedTokenId(firstContractConfig, firstUid); + + assertEq(contractAddress, resultContractAddress); + assertEq(tokenId, newTokenId); + + premintConfig.uid = secondUid; + (resultContractAddress, newTokenId) = _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); + tokenId = preminter.getPremintedTokenId(firstContractConfig, secondUid); + + assertEq(contractAddress, resultContractAddress); + assertEq(tokenId, newTokenId); + + premintConfig.contractConfig = secondContractConfig; + + (resultContractAddress, newTokenId) = _signAndExecutePremint(premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); + contractAddress = preminter.getContractAddress(secondContractConfig); + tokenId = preminter.getPremintedTokenId(secondContractConfig, secondUid); + + assertEq(contractAddress, resultContractAddress); + assertEq(tokenId, newTokenId); + } + + function test_premintCanOnlyBeExecutedAfterStartDate(uint8 startDate, uint8 currentTime) external { + bool shouldRevert; + if (startDate == 0) { + shouldRevert = false; + } else { + // should revert if before the start date + shouldRevert = currentTime < startDate; + } + vm.warp(currentTime); + + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + premintConfig.tokenConfig.mintStart = startDate; + + uint256 quantityToMint = 4; + uint256 chainId = block.chainid; + address premintExecutor = vm.addr(701); + string memory comment = "I love it"; + + // get signature for the premint: + bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + + if (shouldRevert) { + vm.expectRevert(ZoraCreator1155Preminter.MintNotYetStarted.selector); + } + vm.prank(premintExecutor); + preminter.premint(premintConfig, signature, quantityToMint, comment); + } + + function test_premintCanOnlyBeExecutedUpToDurationFromFirstMint(uint8 startDate, uint8 duration, uint8 timeOfFirstMint, uint8 timeOfSecondMint) external { + vm.assume(timeOfFirstMint >= startDate); + vm.assume(timeOfSecondMint >= timeOfFirstMint); + + bool shouldRevert; + if (duration == 0) { + shouldRevert = false; + } else { + // should revert if after the duration + shouldRevert = uint16(timeOfSecondMint) > uint16(timeOfFirstMint) + duration; + } + + // build a premint with a token that has the given start date and duration + ZoraCreator1155Preminter.PremintConfig memory premintConfig = makeDefaultPremintConfig(); + premintConfig.tokenConfig.mintStart = startDate; + premintConfig.tokenConfig.mintDuration = duration; + + uint256 chainId = block.chainid; + + // get signature for the premint: + bytes memory signature = _signPremint(premintConfig, creatorPrivateKey, chainId); + + uint256 quantityToMint = 2; + address premintExecutor = vm.addr(701); + string memory comment = "I love it"; + + vm.startPrank(premintExecutor); + + vm.warp(timeOfFirstMint); + (address contractAddress, uint256 tokenId) = preminter.premint(premintConfig, signature, quantityToMint, comment); + + vm.warp(timeOfSecondMint); + + // execute mint directly on the contract - and check make sure it reverts if minted after sale start + IMinter1155 fixedPriceMinter = factory.defaultMinters()[0]; + if (shouldRevert) { + vm.expectRevert(ZoraCreatorFixedPriceSaleStrategy.SaleEnded.selector); + } + IZoraCreator1155(contractAddress).mint(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment)); + } + + function _signAndExecutePremint( + ZoraCreator1155Preminter.PremintConfig memory premintConfig, + uint256 privateKey, + uint256 chainId, + address executor, + uint256 quantityToMint, + string memory comment + ) private returns (address, uint256) { + bytes memory signature = _signPremint(premintConfig, privateKey, chainId); + + // now call the premint function, using the same config that was used to generate the digest, and the signature + vm.prank(executor); + return preminter.premint(premintConfig, signature, quantityToMint, comment); + } + + function _signPremint( + ZoraCreator1155Preminter.PremintConfig memory premintConfig, + uint256 privateKey, + uint256 chainId + ) private view returns (bytes memory) { + bytes32 digest = preminter.premintHashData(premintConfig, address(preminter), chainId); + + // 3. Sign the digest + // create a signature with the digest for the params + return _sign(privateKey, digest); + } + + function _sign(uint256 privateKey, bytes32 digest) private pure returns (bytes memory) { + // sign the message + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + + // combine into a single bytes array + return abi.encodePacked(r, s, v); + } +} diff --git a/uml/gasslessCreate-collecting-activity.puml b/uml/gasslessCreate-collecting-activity.puml new file mode 100644 index 000000000..c0d41f3b5 --- /dev/null +++ b/uml/gasslessCreate-collecting-activity.puml @@ -0,0 +1,20 @@ +@startuml + +title Collecting with a premint signature + +start + +:Load signature by\ncontract hash & uid; +if (sig with contract hash + uid\nalready executed) then (yes) + :Redirect to\nstandard mint page; + :Mint on erc1155 contract; + stop +else (no) + if (contract already created) then (yes) + :Show contract address; + endif + :Mint on premint contract. Submit:\ncontract & token params, uid, signature\nquantity, comment; +endif +stop + +@enduml diff --git a/uml/gasslessCreate-collecting-sequence.puml b/uml/gasslessCreate-collecting-sequence.puml new file mode 100644 index 000000000..2d029e013 --- /dev/null +++ b/uml/gasslessCreate-collecting-sequence.puml @@ -0,0 +1,48 @@ +@startuml +actor Collector +entity PremintCollectPage +boundary SignatureAPI +entity SignatureDB +entity PreminterContract +entity 1155FactoryContract +entity 1155Contract + +Collector -> PremintCollectPage: Open, param is \ncontract hash + token uid +Activate PremintCollectPage +PremintCollectPage -> SignatureAPI: Fetch by contract hash + 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 +PremintCollectPage -> PreminterContract: Check if signature has been used (by contract hash + token uid) +PreminterContract --> PremintCollectPage: Signature has been used or not + +Group signature has been used + + PremintCollectPage -> Collector: Redirect to \nstandard collect page + +end + +Collector -> PremintCollectPage: mint +PremintCollectPage -> Collector: Submit transaction +deactivate PremintCollectPage +Collector -> PreminterContract: Submit premint transaction containing \nsignature, contract creation & token creation params +activate PreminterContract +PreminterContract -> PreminterContract: record signature used;\nrevert if already used + +Group contract doesnt exist + + PreminterContract -> 1155FactoryContract: create contract + 1155FactoryContract -> 1155Contract: create + activate 1155Contract + +end + +PreminterContract -> 1155Contract: create new token +PreminterContract -> 1155Contract: set new token sale parameters +PreminterContract -> 1155Contract: mint tokens to collector + +deactivate PreminterContract +1155Contract --> Collector: Minted tokens +deactivate 1155Contract + +@enduml \ No newline at end of file diff --git a/uml/gasslessCreate-creation-activity.puml b/uml/gasslessCreate-creation-activity.puml new file mode 100644 index 000000000..201f388da --- /dev/null +++ b/uml/gasslessCreate-creation-activity.puml @@ -0,0 +1,27 @@ +@startuml + +title Creating a token signature + +start + +if (new token) then (yes) + if (new contract) then (yes) + :Ask creator for new\ncontract creation params; + if (contract exists\nwith same params) then (yes) + :switch ui to create token\non existing contract; + else (no) + endif + else (no) + :load existing\ncontract creation parameters\nby hash id; + endif + :Get new uid\nfrom backend server; +else (no) + :load existing\ncontract + token creation parameters\nby contract hash + 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; + +stop + +@enduml diff --git a/uml/gasslessCreate-creation-sequence.puml b/uml/gasslessCreate-creation-sequence.puml new file mode 100644 index 000000000..2d7bc99fa --- /dev/null +++ b/uml/gasslessCreate-creation-sequence.puml @@ -0,0 +1,41 @@ +@startuml + +title Creating a signature for a new erc1155 contract + token + +actor Creator +entity CreatePage +boundary SignatureAPI +entity SignatureDB + + +Group Signature not created for contract yet + + activate CreatePage + Creator -> CreatePage: setup NEW contract name + image + CreatePage -> SignatureAPI: validate that contract \nwith same params for\ncreator doesnt exist + SignatureAPI -> SignatureDB: check if signature with hash \nfor contract is already stored + SignatureAPI --> CreatePage: validation results + +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 + 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 +SignatureDB --> SignatureAPI: next token uid +SignatureAPI --> CreatePage: next token uid +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 + +@enduml \ No newline at end of file diff --git a/uml/generated/gasslessCreate-collecting-activity.svg b/uml/generated/gasslessCreate-collecting-activity.svg new file mode 100644 index 000000000..141d18db0 --- /dev/null +++ b/uml/generated/gasslessCreate-collecting-activity.svg @@ -0,0 +1 @@ +Collecting with a premint signatureLoad signature bycontract hash & uidsig with contract hash + uidalready executedyesnoRedirect tostandard mint pageMint on erc1155 contractShow contract addressyescontract already createdMint on premint contract. Submit:contract & token params, uid, signaturequantity, comment \ No newline at end of file diff --git a/uml/generated/gasslessCreate-collecting-sequence.svg b/uml/generated/gasslessCreate-collecting-sequence.svg new file mode 100644 index 000000000..87f3d5eed --- /dev/null +++ b/uml/generated/gasslessCreate-collecting-sequence.svg @@ -0,0 +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 diff --git a/uml/generated/gasslessCreate-creation-activity.svg b/uml/generated/gasslessCreate-creation-activity.svg new file mode 100644 index 000000000..6dbd1626a --- /dev/null +++ b/uml/generated/gasslessCreate-creation-activity.svg @@ -0,0 +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 diff --git a/uml/generated/gasslessCreate-creation-sequence.svg b/uml/generated/gasslessCreate-creation-sequence.svg new file mode 100644 index 000000000..9850b4315 --- /dev/null +++ b/uml/generated/gasslessCreate-creation-sequence.svg @@ -0,0 +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 diff --git a/wagmi.config.ts b/wagmi.config.ts index 14a93ba76..0733ea6c4 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -8,7 +8,8 @@ type ContractNames = | "ZoraCreatorFixedPriceSaleStrategy" | "ZoraCreatorMerkleMinterStrategy" | "ZoraCreatorRedeemMinterFactory" - | "ZoraCreatorRedeemMinterStrategy"; + | "ZoraCreatorRedeemMinterStrategy" + | "ZoraCreator1155Preminter"; type Address = `0x${string}`; @@ -19,6 +20,7 @@ const contractFilesToInclude: ContractNames[] = [ "ZoraCreatorMerkleMinterStrategy", "ZoraCreatorRedeemMinterFactory", "ZoraCreatorRedeemMinterStrategy", + "ZoraCreator1155Preminter", ]; type Addresses = { @@ -35,8 +37,9 @@ const getAddresses = () => { const addAddress = ( contractName: ContractNames, chainId: number, - address: Address + address?: Address ) => { + if (!address) return; if (!addresses[contractName]) { addresses[contractName] = {}; } @@ -54,6 +57,7 @@ const getAddresses = () => { "1155_IMPL": Address; FACTORY_IMPL: Address; FACTORY_PROXY: Address; + PREMINTER?: Address; }; const chainId = parseInt(addressesFile.split(".")[0]); @@ -78,6 +82,7 @@ const getAddresses = () => { chainId, jsonAddress.REDEEM_MINTER_FACTORY ); + addAddress("ZoraCreator1155Preminter", chainId, jsonAddress.PREMINTER); } return addresses; diff --git a/yarn.lock b/yarn.lock index 117e0ff7b..3704e11da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,32 +8,33 @@ integrity sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ== "@babel/code-frame@^7.0.0": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" - integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" + integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== dependencies: - "@babel/highlight" "^7.22.5" + "@babel/highlight" "^7.22.10" + chalk "^2.4.2" "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== -"@babel/highlight@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" - integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== +"@babel/highlight@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" + integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== dependencies: "@babel/helper-validator-identifier" "^7.22.5" - chalk "^2.0.0" + chalk "^2.4.2" js-tokens "^4.0.0" "@babel/runtime@^7.20.1", "@babel/runtime@^7.5.5": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" - integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" + integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== dependencies: - regenerator-runtime "^0.13.11" + regenerator-runtime "^0.14.0" "@changesets/apply-release-plan@^6.1.4": version "6.1.4" @@ -486,33 +487,28 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.15": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@^0.3.9": - version "0.3.18" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" - integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@manypkg/find-root@^1.1.0": version "1.1.0" @@ -536,13 +532,6 @@ globby "^11.0.0" read-yaml-file "^1.1.0" -"@noble/curves@1.0.0", "@noble/curves@~1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" - integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== - dependencies: - "@noble/hashes" "1.3.0" - "@noble/curves@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" @@ -550,6 +539,13 @@ dependencies: "@noble/hashes" "1.3.1" +"@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + "@noble/hashes@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" @@ -644,7 +640,7 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/node@*": +"@types/node@*", "@types/node@^20.1.2": version "20.5.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313" integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q== @@ -654,11 +650,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== -"@types/node@^20.1.2": - version "20.4.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69" - integrity sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg== - "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -1066,7 +1057,7 @@ chai@^4.3.7: pathval "^1.1.1" type-detect "^4.0.5" -chalk@^2.0.0, chalk@^2.1.0: +chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1400,11 +1391,12 @@ emoji-regex@^9.2.2: integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== enquirer@^2.3.0: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== dependencies: ansi-colors "^4.1.1" + strip-ansi "^6.0.1" error-ex@^1.3.1: version "1.3.2" @@ -2180,10 +2172,10 @@ is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.11.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" - integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== dependencies: has "^1.0.3" @@ -2329,9 +2321,9 @@ isomorphic-ws@5.0.0: integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== jackspeak@^2.0.3: - version "2.2.2" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.2.tgz#707c62733924b8dc2a0a629dc6248577788b5385" - integrity sha512-mgNtVv4vUuaKA97yxUHoA3+FkuhtxkjdXEWOyB/N76fjy0FjezEt34oy3epBtvCvS+7DyKwqCFWx/oJLV5+kCg== + version "2.3.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.0.tgz#aa228a94de830f31d4e4f0184427ce91c4ff1493" + integrity sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: @@ -2407,9 +2399,9 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== listr2@^6.4.2: - version "6.6.0" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-6.6.0.tgz#cb8a0f45fb93ae50c43fb34f934759f8de9395ce" - integrity sha512-qkLg7IeYcZGkxo5sZzl676xHwQzNZ8qAQLQSDMA88sLM1SDcabwyXD1mXHi/PGQHyt/mu81adJdkqsCSUSuQzQ== + version "6.6.1" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-6.6.1.tgz#08b2329e7e8ba6298481464937099f4a2cd7f95d" + integrity sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg== dependencies: cli-truncate "^3.1.0" colorette "^2.0.20" @@ -2523,9 +2515,9 @@ lru-cache@^6.0.0: yallist "^4.0.0" "lru-cache@^9.1.1 || ^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61" - integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw== + version "10.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" + integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== magic-string@^0.30.0: version "0.30.2" @@ -2625,9 +2617,9 @@ minimist-options@^4.0.2: kind-of "^6.0.3" "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e" - integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA== + version "7.0.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" + integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== mixme@^0.5.1: version "0.5.9" @@ -3095,10 +3087,10 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== regexp.prototype.flags@^1.5.0: version "1.5.0" @@ -3125,11 +3117,11 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve@^1.10.0: - version "1.22.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== dependencies: - is-core-module "^2.11.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -3151,14 +3143,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rollup@^3.2.5: - version "3.26.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.26.3.tgz#bbc8818cadd0aebca348dbb3d68d296d220967b8" - integrity sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ== - optionalDependencies: - fsevents "~2.3.2" - -rollup@^3.27.1: +rollup@^3.2.5, rollup@^3.27.1: version "3.28.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.28.0.tgz#a3c70004b01934760c0cb8df717c7a1d932389a2" integrity sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw== @@ -3271,9 +3256,9 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" - integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== slash@^3.0.0: version "3.0.0" @@ -3318,6 +3303,11 @@ solidity-comments-extractor@^0.0.7: resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== +solmate@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/solmate/-/solmate-6.2.0.tgz#edd29b5f3d6faafafdcf65fe4d1d959b4841cfa8" + integrity sha512-AM38ioQ2P8zRsA42zenb9or6OybRjOLXIu3lhIT8rhddUuduCt76pUEuLxOIg9GByGojGz+EbpFdCB6B+QZVVA== + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -3759,22 +3749,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -viem@^1.0.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.4.1.tgz#490f39b3f371bf58910b1b338c237e19066805cf" - integrity sha512-MtaoBHDSJDqa+QyXKG5d+S6EQSebRO0tzw6anSP4zC7AbC614vMeg9Y8LbkmEkWCw8swFYkort+H9l7GkWB0uA== - dependencies: - "@adraffy/ens-normalize" "1.9.0" - "@noble/curves" "1.0.0" - "@noble/hashes" "1.3.0" - "@scure/bip32" "1.3.0" - "@scure/bip39" "1.2.0" - "@wagmi/chains" "1.6.0" - abitype "0.9.3" - isomorphic-ws "5.0.0" - ws "8.12.0" - -viem@^1.6.0: +viem@^1.0.0, viem@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/viem/-/viem-1.6.0.tgz#8befa678c3ac79b9558dfd1708130b2ecb1994f4" integrity sha512-ae9Twkd0q2Qlj4yYpWjb4DzYAhKY0ibEpRH8FJaTywZXNpTjFidSdBaT0CVn1BaH7O7cnX4/O47zvDUMGJD1AA== @@ -4049,6 +4024,6 @@ yocto-queue@^1.0.0: integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== zod@^3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + version "3.22.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.1.tgz#815f850baf933fef96c1061322dbe579b1a80c27" + integrity sha512-+qUhAMl414+Elh+fRNtpU+byrwjDFOS1N7NioLY+tSlcADTx4TkCUua/hxJvxwDXcV4397/nZ420jy4n4+3WUg== From fd304266a3939c3fd895db0c3a2495dd05a6e3fc Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 22 Aug 2023 10:37:30 -0700 Subject: [PATCH 03/18] 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; From 534acae11cd5e3641ddebde2478d0b444afdb45f Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 23 Aug 2023 13:52:16 -0700 Subject: [PATCH 04/18] fix hanging test --- test/factory/ZoraCreator1155Factory.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/factory/ZoraCreator1155Factory.t.sol b/test/factory/ZoraCreator1155Factory.t.sol index bcaa7a43b..71f590b3e 100644 --- a/test/factory/ZoraCreator1155Factory.t.sol +++ b/test/factory/ZoraCreator1155Factory.t.sol @@ -135,6 +135,7 @@ contract ZoraCreator1155FactoryTest is Test { uint16 numberOfCallsBeforeCreation ) external { vm.assume(contractAdmin != address(0)); + vm.assume(numberOfCallsBeforeCreation < 5); address contractCreator = vm.addr(1); From f3b0ad4bd952d484071f6c8957f4ac6e02521ecb Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 23 Aug 2023 13:58:00 -0700 Subject: [PATCH 05/18] prettier fix --- package/preminter.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package/preminter.test.ts b/package/preminter.test.ts index 9dd383b78..100e46386 100644 --- a/package/preminter.test.ts +++ b/package/preminter.test.ts @@ -222,7 +222,9 @@ describe("ZoraCreator1155Preminter", () => { contractAdmin: creatorAccount, }); - const preminterAddress = zoraCreator1155PremintExecutorAddress[forkedChainId as keyof typeof zoraCreator1155PremintExecutorAddress] as Address; + const preminterAddress = zoraCreator1155PremintExecutorAddress[ + forkedChainId as keyof typeof zoraCreator1155PremintExecutorAddress + ] as Address; const contractAddress = await publicClient.readContract({ abi: preminterAbi, @@ -245,8 +247,8 @@ describe("ZoraCreator1155Preminter", () => { signedMessage, contractConfig, premintConfig, - contractAddress - }); + contractAddress, + }); }, 20 * 1000 ); @@ -472,5 +474,4 @@ describe("ZoraCreator1155Preminter", () => { // 10 second timeout 40 * 1000 ); - }); From fe84eb704327be5e211870f4f2b42c55d82c40c4 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 16 Aug 2023 11:45:33 -0700 Subject: [PATCH 06/18] Creator attribution deployed contracts --- addresses/999.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/addresses/999.json b/addresses/999.json index 6c0ff5490..1d97a50e2 100644 --- a/addresses/999.json +++ b/addresses/999.json @@ -1,10 +1,11 @@ { - "CONTRACT_1155_IMPL": "0x91C1863eD54809c45b53bb6090eb437036c792C4", - "FACTORY_IMPL": "0xdF4A315443Ce2c11e6657D6A98B3a7143DE7B268", - "FACTORY_PROXY": "0x6a357139C1bcDcf0B3AB9bC447932dDdcb956703", + "CONTRACT_1155_IMPL": "0xe349c0e2C91DE151F7Fc469BF9c39F8A05EF59D2", + "FACTORY_IMPL": "0x93B2BCa3b7734370BB5bdcaB5Ef83b8E995cb9e0", + "FACTORY_PROXY": "0x368d9Fa8Fe07652a73E91986A7Eee2436D218739", "FIXED_PRICE_SALE_STRATEGY": "0xd81351363b7d80b06E4Ec4De7989f0f91e41A846", "MERKLE_MINT_SALE_STRATEGY": "0x2c4457D38A329526063b26a2bB2C31B61553Aa98", + "PREMINTER": "0xD8E48130D64005fe77915c21B02FCcd357BD97F4", "REDEEM_MINTER_FACTORY": "0x27817bAef1341De9Ad04097Bbba4Ea8dA32c8552", - "timestamp": 1688509842, - "commit": "f60dd5f" -} + "timestamp": 1692290856, + "commit": "3169373" +} \ No newline at end of file From bf9f9b519b0585e81c8d5578259708bca74fe787 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 23 Aug 2023 12:58:37 -0700 Subject: [PATCH 07/18] hack - only test fork on zora goerli for premint --- test/premint/ZoraCreator1155Preminter.t.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index 399b5985e..376624818 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -156,6 +156,10 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { } function testTheForkPremint(string memory chainName) private { + if (keccak256(abi.encodePacked(chainName)) != keccak256(abi.encodePacked("zora_goerli"))) { + return; + } + console.log("testing on fork: ", chainName); // create and select the fork, which will be used for all subsequent calls From 34db10002bb2f544fdc0758d94be3f09ffbc0f7f Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 18 Aug 2023 12:30:35 -0700 Subject: [PATCH 08/18] deployed new creator attribution Created a release --- .env.anvil | 2 +- addresses/999.json | 6 +++--- package.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env.anvil b/.env.anvil index e1129784f..0c2078aa6 100644 --- a/.env.anvil +++ b/.env.anvil @@ -1,2 +1,2 @@ FORK_RPC_URL="https://testnet.rpc.zora.co/" -FORK_BLOCK_NUMBER=906028 \ No newline at end of file +FORK_BLOCK_NUMBER=916572 \ No newline at end of file diff --git a/addresses/999.json b/addresses/999.json index 1d97a50e2..f8a2530c1 100644 --- a/addresses/999.json +++ b/addresses/999.json @@ -4,8 +4,8 @@ "FACTORY_PROXY": "0x368d9Fa8Fe07652a73E91986A7Eee2436D218739", "FIXED_PRICE_SALE_STRATEGY": "0xd81351363b7d80b06E4Ec4De7989f0f91e41A846", "MERKLE_MINT_SALE_STRATEGY": "0x2c4457D38A329526063b26a2bB2C31B61553Aa98", - "PREMINTER": "0xD8E48130D64005fe77915c21B02FCcd357BD97F4", + "PREMINTER": "0x9D0162AC5ff579670f9b941db7eb7675667883B5", "REDEEM_MINTER_FACTORY": "0x27817bAef1341De9Ad04097Bbba4Ea8dA32c8552", - "timestamp": 1692290856, - "commit": "3169373" + "timestamp": 1692386956, + "commit": "d077c85" } \ No newline at end of file diff --git a/package.json b/package.json index 45b769ad2..7c7db26da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zoralabs/zora-1155-contracts", - "version": "1.4.0", + "version": "1.4.5-gasless", "repository": "git@github.com:ourzora/creator-contracts.git", "author": "Iain ", "license": "MIT", @@ -14,7 +14,7 @@ "prettier": "prettier --write 'src/**/*.sol' 'test/**/*.sol' 'package/**/*.ts' 'wagmi.config.ts'", "coverage": "forge coverage --report lcov", "write-gas-report": "forge test --gas-report > gasreport.ansi", - "prepack": "node script/copy-deployed-contracts.mjs && yarn wagmi && yarn bundle-configs && yarn build", + "prepack": "yarn wagmi && yarn bundle-configs && yarn build", "update-new-deployment-addresses": "node script/copy-deployed-contracts.mjs deploy", "build": "tsup", "bundle-configs": "node script/bundle-chainConfigs.mjs && yarn prettier", From 2aba02ba157ee3254cec86ea49fb6f78557fb2ff Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Mon, 28 Aug 2023 17:05:31 -0400 Subject: [PATCH 09/18] fix: factory tests --- script/DeployScript.s.sol | 68 +++++++++++++++++++++++ test/factory/ZoraCreator1155Factory.t.sol | 65 +++++++++++++--------- 2 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 script/DeployScript.s.sol diff --git a/script/DeployScript.s.sol b/script/DeployScript.s.sol new file mode 100644 index 000000000..81baceab1 --- /dev/null +++ b/script/DeployScript.s.sol @@ -0,0 +1,68 @@ +// 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"; + +contract DeployScript is ZoraDeployerBase { + address deployer; + uint256 deployerPK; + + function setUp() public { + deployer = vm.envAddress("DEPLOYER"); + deployerPK = vm.envUint("DEPLOYER_PK"); + } + + function run() public { + Deployment memory deployment = getDeployment(); + ChainConfig memory chainConfig = getChainConfig(); + + console2.log("~~~ CHAIN CONFIG ~~~"); + console2.log("chainId", chainId()); + console2.log("protocolRewards", chainConfig.protocolRewards); + + console2.log(""); + + ZoraCreatorFixedPriceSaleStrategy fixedPricedMinter = + ZoraCreatorFixedPriceSaleStrategy(deployment.fixedPriceSaleStrategy); + ZoraCreatorMerkleMinterStrategy merkleMinter = + ZoraCreatorMerkleMinterStrategy(deployment.merkleMintSaleStrategy); + ZoraCreatorRedeemMinterFactory redeemMinterFactory = + ZoraCreatorRedeemMinterFactory(deployment.redeemMinterFactory); + + vm.startBroadcast(deployerPK); + + ZoraCreator1155Impl creatorImpl = + new ZoraCreator1155Impl(chainConfig.mintFeeAmount, chainConfig.mintFeeRecipient, deployment.factoryProxy, chainConfig.protocolRewards); + + ZoraCreator1155FactoryImpl newFactoryImpl = new ZoraCreator1155FactoryImpl({ + _implementation: creatorImpl, + _merkleMinter: merkleMinter, + _redeemMinterFactory: redeemMinterFactory, + _fixedPriceMinter: fixedPricedMinter + }); + + vm.stopBroadcast(); + + console2.log(""); + console2.log("SAFE:", chainConfig.factoryOwner); + console2.log("PROXY:", deployment.factoryProxy); + console2.log("NEW FACTORY IMPL:", address(newFactoryImpl)); + console2.log("NEW 1155 IMPL:", address(creatorImpl)); + } +} diff --git a/test/factory/ZoraCreator1155Factory.t.sol b/test/factory/ZoraCreator1155Factory.t.sol index 71f590b3e..8e79b0584 100644 --- a/test/factory/ZoraCreator1155Factory.t.sol +++ b/test/factory/ZoraCreator1155Factory.t.sol @@ -12,16 +12,32 @@ import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {MockContractMetadata} from "../mock/MockContractMetadata.sol"; +import {ProxyShim} from "../../src/utils/ProxyShim.sol"; contract ZoraCreator1155FactoryTest is Test { - ZoraCreator1155FactoryImpl internal factory; address internal zora; + uint256 internal mintFeeAmount; + + ZoraCreator1155FactoryImpl internal factoryImpl; + ZoraCreator1155FactoryImpl internal factory; function setUp() external { zora = makeAddr("zora"); + mintFeeAmount = 0.000777 ether; + + address factoryShimAddress = address(new ProxyShim(zora)); + Zora1155Factory factoryProxy = new Zora1155Factory(factoryShimAddress, ""); + ProtocolRewards protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); - factory = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), IMinter1155(address(2)), IMinter1155(address(3))); + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(protocolRewards)); + + factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), IMinter1155(address(2)), IMinter1155(address(3))); + factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); + + vm.startPrank(zora); + factory.upgradeTo(address(factoryImpl)); + factory.initialize(zora); + vm.stopPrank(); } function test_contractVersion() external { @@ -39,7 +55,7 @@ contract ZoraCreator1155FactoryTest is Test { function test_initialize(address initialOwner) external { vm.assume(initialOwner != address(0)); address payable proxyAddress = payable( - address(new Zora1155Factory(address(factory), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, initialOwner))) + address(new Zora1155Factory(address(factoryImpl), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, initialOwner))) ); ZoraCreator1155FactoryImpl proxy = ZoraCreator1155FactoryImpl(proxyAddress); assertEq(proxy.owner(), initialOwner); @@ -104,7 +120,7 @@ contract ZoraCreator1155FactoryTest is Test { ); address payable proxyAddress = payable( - address(new Zora1155Factory(address(factory), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, initialOwner))) + address(new Zora1155Factory(address(factoryImpl), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, initialOwner))) ); ZoraCreator1155FactoryImpl proxy = ZoraCreator1155FactoryImpl(proxyAddress); vm.prank(initialOwner); @@ -118,7 +134,7 @@ contract ZoraCreator1155FactoryTest is Test { MockContractMetadata mockContractMetadata = new MockContractMetadata("ipfs://asdfadsf", "name"); address payable proxyAddress = payable( - address(new Zora1155Factory(address(factory), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, initialOwner))) + address(new Zora1155Factory(address(factoryImpl), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, initialOwner))) ); ZoraCreator1155FactoryImpl proxy = ZoraCreator1155FactoryImpl(proxyAddress); vm.prank(initialOwner); @@ -126,7 +142,7 @@ contract ZoraCreator1155FactoryTest is Test { proxy.upgradeTo(address(mockContractMetadata)); } - function test_createContractDeterminisitc_createsContractAtSameAddressForNameAndUri( + function test_createContractDeterministic_createsContractAtSameAddressForNameAndUri( string calldata nameA, string calldata uri, address contractAdmin, @@ -167,27 +183,18 @@ contract ZoraCreator1155FactoryTest is Test { string memory uri = "ipfs://asdfadsf"; string memory nameA = "nameA"; address contractAdmin = vm.addr(1); - - address factoryOwner = vm.addr(10); - // account that creates the contract (not necessarily the owner/admin) address contractCreator = vm.addr(2); - // 1. create the proxy, pointing it to the factory implentation and setting the owner - ZoraCreator1155FactoryImpl proxy = ZoraCreator1155FactoryImpl( - payable(new Zora1155Factory(address(factory), abi.encodeWithSelector(ZoraCreator1155FactoryImpl.initialize.selector, factoryOwner))) - ); - - // get the deterministic address of the contract before its created - address expectedContractAddress = proxy.deterministicContractAddress(contractCreator, uri, nameA, contractAdmin); - - uint256 newMintFeeAmount = 1 ether; + // 1. get the deterministic address of the contract before its created, from the existing factory proxy + address expectedContractAddress = factory.deterministicContractAddress(contractCreator, uri, nameA, contractAdmin); // 2. update the erc1155 implementation: // * create a new version of the erc1155 implementation // * create a new factory that points to that new erc1155 implementation, // * upgrade the proxy to point to the new factory - IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, address(0), address(0), address(new ProtocolRewards())); + uint256 newMintFeeAmount = 0.000888 ether; + IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, zora, address(factory), address(new ProtocolRewards())); ZoraCreator1155FactoryImpl newFactoryImpl = new ZoraCreator1155FactoryImpl( newZoraCreator, @@ -196,11 +203,11 @@ contract ZoraCreator1155FactoryTest is Test { IMinter1155(address(0)) ); - vm.prank(factoryOwner); - proxy.upgradeTo(address(newFactoryImpl)); + vm.prank(zora); + factory.upgradeTo(address(newFactoryImpl)); // sanity check - make sure that the proxy erc1155 implementation is pointing to the new implementation - assertEq(address(proxy.implementation()), address(newZoraCreator)); + assertEq(address(factory.implementation()), address(newZoraCreator)); // 3. Create a contract with a deterministic address, it should match the address from before the upgrade ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ @@ -213,7 +220,7 @@ contract ZoraCreator1155FactoryTest is Test { // now create deterministically, address should match expected address vm.prank(contractCreator); - address createdAddress = proxy.createContractDeterministic(uri, nameA, royaltyConfig, payable(contractAdmin), initSetup); + address createdAddress = factory.createContractDeterministic(uri, nameA, royaltyConfig, payable(contractAdmin), initSetup); assertEq(createdAddress, expectedContractAddress); } @@ -238,8 +245,14 @@ contract ZoraCreator1155FactoryTest is Test { ZoraCreator1155Impl creatorProxy = ZoraCreator1155Impl(createdAddress); // 2. upgrade the created contract by creating a new contract and upgrading the existing one to point to it. - uint256 newMintFeeAmount = 1 ether; - IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, address(0), address(0), address(new ProtocolRewards())); + uint256 newMintFeeAmount = 0.000888 ether; + IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, zora, address(0), address(new ProtocolRewards())); + + address[] memory baseImpls = new address[](1); + baseImpls[0] = address(factory.implementation()); + + vm.prank(zora); + factory.registerUpgradePath(baseImpls, address(newZoraCreator)); vm.prank(creatorProxy.owner()); creatorProxy.upgradeTo(address(newZoraCreator)); From 2bc790cc4034ab73205e8694d66f7843cfea6839 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 13:26:07 -0400 Subject: [PATCH 10/18] chore: remove alt deploy script --- script/DeployScript.s.sol | 68 --------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 script/DeployScript.s.sol diff --git a/script/DeployScript.s.sol b/script/DeployScript.s.sol deleted file mode 100644 index 81baceab1..000000000 --- a/script/DeployScript.s.sol +++ /dev/null @@ -1,68 +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"; - -contract DeployScript is ZoraDeployerBase { - address deployer; - uint256 deployerPK; - - function setUp() public { - deployer = vm.envAddress("DEPLOYER"); - deployerPK = vm.envUint("DEPLOYER_PK"); - } - - function run() public { - Deployment memory deployment = getDeployment(); - ChainConfig memory chainConfig = getChainConfig(); - - console2.log("~~~ CHAIN CONFIG ~~~"); - console2.log("chainId", chainId()); - console2.log("protocolRewards", chainConfig.protocolRewards); - - console2.log(""); - - ZoraCreatorFixedPriceSaleStrategy fixedPricedMinter = - ZoraCreatorFixedPriceSaleStrategy(deployment.fixedPriceSaleStrategy); - ZoraCreatorMerkleMinterStrategy merkleMinter = - ZoraCreatorMerkleMinterStrategy(deployment.merkleMintSaleStrategy); - ZoraCreatorRedeemMinterFactory redeemMinterFactory = - ZoraCreatorRedeemMinterFactory(deployment.redeemMinterFactory); - - vm.startBroadcast(deployerPK); - - ZoraCreator1155Impl creatorImpl = - new ZoraCreator1155Impl(chainConfig.mintFeeAmount, chainConfig.mintFeeRecipient, deployment.factoryProxy, chainConfig.protocolRewards); - - ZoraCreator1155FactoryImpl newFactoryImpl = new ZoraCreator1155FactoryImpl({ - _implementation: creatorImpl, - _merkleMinter: merkleMinter, - _redeemMinterFactory: redeemMinterFactory, - _fixedPriceMinter: fixedPricedMinter - }); - - vm.stopBroadcast(); - - console2.log(""); - console2.log("SAFE:", chainConfig.factoryOwner); - console2.log("PROXY:", deployment.factoryProxy); - console2.log("NEW FACTORY IMPL:", address(newFactoryImpl)); - console2.log("NEW 1155 IMPL:", address(creatorImpl)); - } -} From f9f33609863a1d5a045f9af15f748d8cf8e4bcb8 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 18:35:54 -0400 Subject: [PATCH 11/18] refactor: early return token id if token exists --- src/nft/ZoraCreator1155Impl.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/nft/ZoraCreator1155Impl.sol b/src/nft/ZoraCreator1155Impl.sol index ee46e35c3..e554f6641 100644 --- a/src/nft/ZoraCreator1155Impl.sol +++ b/src/nft/ZoraCreator1155Impl.sol @@ -732,6 +732,12 @@ contract ZoraCreator1155Impl is error PremintAlreadyExecuted(); function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature) public nonReentrant returns (uint256 newTokenId) { + // if a token has already been created for a premint config with this uid: + if (delegatedTokenId[premintConfig.uid] != 0) { + // return its token id + return delegatedTokenId[premintConfig.uid]; + } + bytes32 hashedPremintConfig = ZoraCreator1155Attribution.validateAndHashPremint(premintConfig); // this is what attributes this token to have been created by the original creator @@ -743,11 +749,6 @@ contract ZoraCreator1155Impl is // 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); From f9ceeae8e7138c9e9f85f70a87532c84cf3a6d3f Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 18:54:40 -0400 Subject: [PATCH 12/18] refactor: store 1155 factory as immutable --- src/premint/ZoraCreator1155PremintExecutor.sol | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index f615940f4..5c7a34862 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -16,7 +16,7 @@ import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1 /// Mints the first x tokens to the executor of the transaction. /// @author @oveddan contract ZoraCreator1155PremintExecutor { - IZoraCreator1155Factory factory; + IZoraCreator1155Factory public immutable zora1155Factory; /// @notice copied from SharedBaseConstants uint256 constant CONTRACT_BASE_ID = 0; @@ -27,7 +27,7 @@ contract ZoraCreator1155PremintExecutor { error InvalidSignature(); constructor(IZoraCreator1155Factory _factory) { - factory = _factory; + zora1155Factory = _factory; } event Preminted( @@ -61,25 +61,21 @@ contract ZoraCreator1155PremintExecutor { // 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) + abi.encode(msg.sender, mintComment) ); // emit Preminted event - emit Preminted(contractAddress, newTokenId, isNewContract, premintConfig.uid, contractConfig, premintConfig.tokenConfig, msg.sender, quantityToMint); + emit Preminted(address(tokenContract), newTokenId, isNewContract, premintConfig.uid, contractConfig, premintConfig.tokenConfig, msg.sender, quantityToMint); } function _getOrCreateContract(ContractCreationConfig calldata contractConfig) private returns (IZoraCreator1155 tokenContract, bool isNewContract) { @@ -100,7 +96,7 @@ contract ZoraCreator1155PremintExecutor { bytes[] memory setupActions = new bytes[](0); // create the contract via the factory. - address newContractAddresss = factory.createContractDeterministic( + address newContractAddresss = zora1155Factory.createContractDeterministic( contractConfig.contractURI, contractConfig.contractName, // default royalty config is empty, since we set it on a token level @@ -115,7 +111,7 @@ contract ZoraCreator1155PremintExecutor { /// 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); + return zora1155Factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); } /// Recovers the signer of the given premint config created against the specified zora1155 contract address. From d9a9c4931d0b803c8219fcf9ab7693fe82d6d89a Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 18:55:13 -0400 Subject: [PATCH 13/18] chore: update tests --- test/premint/ZoraCreator1155Preminter.t.sol | 183 ++++++++++++-------- 1 file changed, 114 insertions(+), 69 deletions(-) diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index 376624818..a9887595f 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -20,15 +20,25 @@ import {ZoraCreator1155PremintExecutor} from "../../src/premint/ZoraCreator1155P import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Attribution.sol"; import {ForkDeploymentConfig} from "../../src/deployment/DeploymentConfig.sol"; +import {ProxyShim} from "../../src/utils/ProxyShim.sol"; contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { + uint256 internal constant CONTRACT_BASE_ID = 0; + uint256 internal constant PERMISSION_BIT_MINTER = 2 ** 2; + ZoraCreator1155PremintExecutor internal preminter; + ZoraCreator1155FactoryImpl internal factoryImpl; ZoraCreator1155FactoryImpl internal factory; - // setup contract config - uint256 creatorPrivateKey = 0xA11CE; - address creator; - ICreatorRoyaltiesControl.RoyaltyConfiguration defaultRoyaltyConfig; + ICreatorRoyaltiesControl.RoyaltyConfiguration internal defaultRoyaltyConfig; + uint256 internal mintFeeAmount; + + // setup contract config + uint256 internal creatorPrivateKey; + address internal creator; + address internal zora; + address internal premintExecutor; + address internal collector; event Preminted( address indexed contractAddress, @@ -42,24 +52,27 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { ); function setUp() external { + mintFeeAmount = 0.000777 ether; + + (creator, creatorPrivateKey) = makeAddrAndKey("creator"); + zora = makeAddr("zora"); + premintExecutor = makeAddr("premintExecutor"); + collector = makeAddr("collector"); + + address factoryShimAddress = address(new ProxyShim(zora)); + Zora1155Factory factoryProxy = new Zora1155Factory(factoryShimAddress, ""); ProtocolRewards rewards = new ProtocolRewards(); - ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(0, makeAddr("zora"), address(0), address(rewards)); + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(rewards)); ZoraCreatorFixedPriceSaleStrategy fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); - factory = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); - uint32 royaltyBPS = 2; - uint32 royaltyMintSchedule = 20; - address royaltyRecipient = vm.addr(4); - - defaultRoyaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration({ - royaltyBPS: royaltyBPS, - royaltyRecipient: royaltyRecipient, - royaltyMintSchedule: royaltyMintSchedule - }); + factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); + factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); - preminter = new ZoraCreator1155PremintExecutor(factory); + vm.startPrank(zora); + factory.upgradeTo(address(factoryImpl)); + factory.initialize(zora); + vm.stopPrank(); - creatorPrivateKey = 0xA11CE; - creator = vm.addr(creatorPrivateKey); + preminter = new ZoraCreator1155PremintExecutor(factory); } function makeDefaultContractCreationConfig() internal view returns (ContractCreationConfig memory) { @@ -67,7 +80,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { } function makeDefaultTokenCreationConfig() internal view returns (TokenCreationConfig memory) { - IMinter1155 fixedPriceMinter = factory.defaultMinters()[0]; + IMinter1155 fixedPriceMinter = factoryImpl.defaultMinters()[0]; return TokenCreationConfig({ tokenURI: "blah.token", @@ -110,13 +123,10 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { // create a signature with the digest for the params bytes memory signature = _sign(creatorPrivateKey, digest); - uint256 mintFee = 0; - - uint256 mintCost = mintFee * quantityToMint; - + uint256 mintCost = mintFeeAmount * 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{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); @@ -135,6 +145,8 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); signature = _sign(creatorPrivateKey, digest); + vm.deal(premintExecutor, mintCost); + // premint with new token config and signature vm.prank(premintExecutor); tokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); @@ -144,6 +156,17 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), quantityToMint); } + // function testMintsTokensToConsecutiveExecutor(uint256 numTokens) public { + // ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + // PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // vm.prank(collector); + // uint256 shouldBeSameTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + + // assertEq(tokenId, shouldBeSameTokenId); + // 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) { @@ -169,7 +192,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { // 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); + factoryImpl = ZoraCreator1155FactoryImpl(getDeployment().factoryImpl); console.log("building defaults"); @@ -196,7 +219,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { 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); + 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); @@ -217,7 +240,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { } } - function test_signatureForSameContractandUid_cannotBeExecutedTwice() external { + function test_signatureForSameContractandUid_shouldMintExistingToken() external { // 1. Make contract creation params // configuration of contract to create @@ -225,36 +248,50 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { PremintConfig memory premintConfig = makeDefaultPremintConfig(); // how many tokens are minted to the executor - uint256 quantityToMint = 4; + uint256 quantityToMint = 2; uint256 chainId = block.chainid; - address premintExecutor = vm.addr(701); string memory comment = "I love it"; address contractAddress = preminter.getContractAddress(contractConfig); - _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); + IZoraCreator1155 created1155Contract = IZoraCreator1155(contractAddress); + + uint256 firstTokenId = _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - // create a sig for another token with same uid, it should revert + // create a sig for another token with same uid, it should mint tokens for the uid's original token premintConfig.tokenConfig.tokenURI = "blah2.token"; 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(ZoraCreator1155Impl.PremintAlreadyExecuted.selector)); - preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 mintCost = mintFeeAmount * quantityToMint; + vm.deal(collector, mintCost); + + uint256 returnedTokenId; + + vm.startPrank(collector); + // premint with new token config and signature, but same uid - it should mint tokens for the first token + returnedTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + + assertEq(returnedTokenId, firstTokenId); + assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint); - // change the version, it should still revert + // change the version, it should still point to the first token premintConfig.version++; signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - // premint with new token config and signature - it should revert - vm.expectRevert(abi.encodeWithSelector(ZoraCreator1155Impl.PremintAlreadyExecuted.selector)); - preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + vm.deal(collector, mintCost); - // change the uid, it should not revert + // premint with new token config and signature - it should mint tokens for the first token + returnedTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + vm.stopPrank(); + + assertEq(returnedTokenId, firstTokenId); + assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint*2); + + // creator signs a new uid, it should create a new token premintConfig.uid++; signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + // preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + } function test_deleted_preventsTokenFromBeingMinted() external { @@ -263,7 +300,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { premintConfig.deleted = true; uint chainId = block.chainid; - address premintExecutor = vm.addr(701); uint256 quantityToMint = 2; string memory comment = "I love it"; @@ -297,11 +333,12 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { 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); - string memory comment = "I love it"; + uint256 mintCost = mintFeeAmount * quantityToMint; + // this account will be used to execute the premint, and should result in a contract being created + vm.deal(premintExecutor, mintCost); + vm.startPrank(premintExecutor); bool createdNewContract = true; @@ -316,7 +353,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { premintExecutor, quantityToMint ); - preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); } function test_onlyOwner_hasAdminRights_onCreatedToken() public { @@ -326,8 +363,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { // how many tokens are minted to the executor uint256 quantityToMint = 4; uint256 chainId = block.chainid; - // this account will be used to execute the premint, and should result in a contract being created - address premintExecutor = vm.addr(701); + string memory comment = "I love it"; address createdContractAddress = preminter.getContractAddress(contractConfig); @@ -345,7 +381,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { fundsRecipient: creator }); - IMinter1155 fixedPrice = factory.fixedPriceMinter(); + IMinter1155 fixedPrice = factoryImpl.fixedPriceMinter(); // have the premint contract try to set the sales config - it should revert with // the expected UserMissingRole error @@ -395,8 +431,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { // how many tokens are minted to the executor uint256 quantityToMint = 4; uint256 chainId = block.chainid; - // this account will be used to execute the premint, and should result in a contract being created - address premintExecutor = vm.addr(701); string memory comment = "I love it"; uint32 firstUid = premintConfig.uid; @@ -465,7 +499,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { uint256 quantityToMint = 4; uint256 chainId = block.chainid; - address premintExecutor = vm.addr(701); string memory comment = "I love it"; // get signature for the premint: @@ -474,8 +507,12 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { if (shouldRevert) { vm.expectRevert(ZoraCreator1155Attribution.MintNotYetStarted.selector); } + + uint256 mintCost = mintFeeAmount * quantityToMint; + vm.deal(premintExecutor, mintCost); + vm.prank(premintExecutor); - preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); } function test_premintCanOnlyBeExecutedUpToDurationFromFirstMint(uint8 startDate, uint8 duration, uint8 timeOfFirstMint, uint8 timeOfSecondMint) external { @@ -504,23 +541,28 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); uint256 quantityToMint = 2; - address premintExecutor = vm.addr(701); string memory comment = "I love it"; + uint256 mintCost = mintFeeAmount * quantityToMint; + vm.deal(premintExecutor, mintCost); + vm.startPrank(premintExecutor); vm.warp(timeOfFirstMint); - uint256 tokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 tokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); vm.warp(timeOfSecondMint); // execute mint directly on the contract - and check make sure it reverts if minted after sale start - IMinter1155 fixedPriceMinter = factory.defaultMinters()[0]; + IMinter1155 fixedPriceMinter = factoryImpl.defaultMinters()[0]; if (shouldRevert) { vm.expectRevert(ZoraCreatorFixedPriceSaleStrategy.SaleEnded.selector); } - IZoraCreator1155(contractAddress).mint(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment)); + vm.deal(premintExecutor, mintCost); + IZoraCreator1155(contractAddress).mint{value: mintCost}(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment)); + + vm.stopPrank(); } function test_premintStatus_getsIfContractHasBeenCreatedAndTokenIdForPremint() external { @@ -550,17 +592,11 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { 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); @@ -568,7 +604,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { assertTrue(isValidSignature); - _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, executor, 1, "hi"); + _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, premintExecutor, 1, "hi"); // contract has been created @@ -586,10 +622,14 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { assertFalse(isValidSignature); + uint256 quantityToMint = 1; + uint256 mintCost = mintFeeAmount * quantityToMint; + vm.deal(premintExecutor, mintCost); + // 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"); + vm.prank(premintExecutor); + preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "yo"); // now grant the new creator permission to mint vm.prank(creator); @@ -599,9 +639,11 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { (isValidSignature, , ) = preminter.isValidSignature(contractConfig, premintConfig2, newCreatorSignature); assertTrue(isValidSignature); + vm.deal(premintExecutor, mintCost); + // try to mint again, should not revert - vm.prank(executor); - preminter.premint(contractConfig, premintConfig2, newCreatorSignature, 1, "yo"); + vm.prank(premintExecutor); + preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "yo"); } function _signAndExecutePremint( @@ -615,9 +657,12 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { ) private returns (uint256 newTokenId) { bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, privateKey, chainId); + uint256 mintCost = mintFeeAmount * quantityToMint; + vm.deal(executor, mintCost); + // now call the premint function, using the same config that was used to generate the digest, and the signature vm.prank(executor); - newTokenId = preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + newTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); } function _signPremint( From 8bcabdf439a8f4acb162802f6d8cda116cf8f363 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Fri, 1 Sep 2023 13:40:06 -0400 Subject: [PATCH 14/18] chore: update tests --- test/premint/ZoraCreator1155Preminter.t.sol | 49 ++++++++++++--------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index a9887595f..f23d941aa 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -156,17 +156,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), quantityToMint); } - // function testMintsTokensToConsecutiveExecutor(uint256 numTokens) public { - // ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - // PremintConfig memory premintConfig = makeDefaultPremintConfig(); - - // vm.prank(collector); - // uint256 shouldBeSameTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - - // assertEq(tokenId, shouldBeSameTokenId); - // 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) { @@ -263,14 +252,14 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { uint256 mintCost = mintFeeAmount * quantityToMint; vm.deal(collector, mintCost); - - uint256 returnedTokenId; + + uint256 nextTokenId; vm.startPrank(collector); // premint with new token config and signature, but same uid - it should mint tokens for the first token - returnedTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - assertEq(returnedTokenId, firstTokenId); + assertEq(nextTokenId, firstTokenId); assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint); // change the version, it should still point to the first token @@ -280,18 +269,38 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { vm.deal(collector, mintCost); // premint with new token config and signature - it should mint tokens for the first token - returnedTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); vm.stopPrank(); - assertEq(returnedTokenId, firstTokenId); - assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint*2); + assertEq(nextTokenId, firstTokenId); + assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint * 2); + } + + function testCreateTokenPerUid() public { + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + uint256 quantityToMint = 2; + uint256 chainId = block.chainid; + string memory comment = "I love it"; + + address contractAddress = preminter.getContractAddress(contractConfig); + + uint256 firstTokenId = _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); // creator signs a new uid, it should create a new token premintConfig.uid++; - signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); + bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - // preminter.premint(contractConfig, premintConfig, signature, quantityToMint, comment); + uint256 mintCost = mintFeeAmount * quantityToMint; + vm.deal(collector, mintCost); + + vm.startPrank(collector); + // premint with new token config and signature, but same uid - it should mint tokens for the first token + uint256 nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + assertEq(firstTokenId, 1); + assertEq(nextTokenId, 2); } function test_deleted_preventsTokenFromBeingMinted() external { From 625c95812300f33fa94f37094c1842e2842ebffa Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Fri, 1 Sep 2023 13:40:20 -0400 Subject: [PATCH 15/18] chore: lint --- src/premint/ZoraCreator1155PremintExecutor.sol | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index 5c7a34862..c310eae51 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -75,7 +75,16 @@ contract ZoraCreator1155PremintExecutor { ); // emit Preminted event - emit Preminted(address(tokenContract), newTokenId, isNewContract, premintConfig.uid, contractConfig, premintConfig.tokenConfig, msg.sender, quantityToMint); + emit Preminted( + address(tokenContract), + newTokenId, + isNewContract, + premintConfig.uid, + contractConfig, + premintConfig.tokenConfig, + msg.sender, + quantityToMint + ); } function _getOrCreateContract(ContractCreationConfig calldata contractConfig) private returns (IZoraCreator1155 tokenContract, bool isNewContract) { @@ -111,7 +120,8 @@ contract ZoraCreator1155PremintExecutor { /// 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 zora1155Factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); + return + zora1155Factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); } /// Recovers the signer of the given premint config created against the specified zora1155 contract address. From d54c4251c9e77ed45a32ff4824105838f3f8d7cd Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Mon, 11 Sep 2023 13:03:40 -0700 Subject: [PATCH 16/18] Premint: fix fork tests (#164) * * Check if key exists reading an address, as to not result in unexpected reverts * In premint tests, allow all forks to run the tests, but skip if there is no address for preminter * update storage layout --- .storage-layout | 1 + package.json | 2 +- src/deployment/DeploymentConfig.sol | 25 +++++++++++++++------ test/premint/ZoraCreator1155Preminter.t.sol | 16 +++++++------ yarn.lock | 6 ++--- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.storage-layout b/.storage-layout index b35d8519c..6859a1da5 100644 --- a/.storage-layout +++ b/.storage-layout @@ -33,6 +33,7 @@ | permissions | mapping(uint256 => mapping(address => uint256)) | 510 | 0 | 32 | src/nft/ZoraCreator1155Impl.sol:ZoraCreator1155Impl | | __gap | uint256[50] | 511 | 0 | 1600 | src/nft/ZoraCreator1155Impl.sol:ZoraCreator1155Impl | | createReferrals | mapping(uint256 => address) | 561 | 0 | 32 | src/nft/ZoraCreator1155Impl.sol:ZoraCreator1155Impl | +| delegatedTokenId | mapping(uint32 => uint256) | 562 | 0 | 32 | src/nft/ZoraCreator1155Impl.sol:ZoraCreator1155Impl | ======================= ➡ ZoraCreator1155FactoryImpl diff --git a/package.json b/package.json index 7c7db26da..d65ab59c6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@zoralabs/openzeppelin-contracts-upgradeable": "4.8.4", "@zoralabs/protocol-rewards": "1.1.1", "ds-test": "https://github.com/dapphub/ds-test#cd98eff28324bfac652e63a239a60632a761790b", - "forge-std": "https://github.com/foundry-rs/forge-std#cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653", + "forge-std": "https://github.com/foundry-rs/forge-std#705263c95892a906d7af65f0f73ce8a4a0c80b80", "solmate": "^6.1.0" }, "devDependencies": { diff --git a/src/deployment/DeploymentConfig.sol b/src/deployment/DeploymentConfig.sol index c43a12077..e71baeae9 100644 --- a/src/deployment/DeploymentConfig.sol +++ b/src/deployment/DeploymentConfig.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.17; import "forge-std/Test.sol"; +import {CommonBase} from "forge-std/Base.sol"; import {MintFeeManager} from "../../src/fee/MintFeeManager.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; @@ -76,17 +77,27 @@ abstract contract DeploymentConfig is CommonBase { chainConfig.protocolRewards = json.readAddress(getKeyPrefix(PROTOCOL_REWARDS)); } + function readAddressOrDefaultToZero(string memory json, string memory key) internal view returns (address addr) { + string memory keyPrefix = getKeyPrefix(key); + + if (vm.keyExists(json, keyPrefix)) { + addr = json.readAddress(keyPrefix); + } else { + addr = address(0); + } + } + /// @notice Get the deployment configuration struct from the JSON configuration file /// @return deployment deployment configuration structure function getDeployment() internal view returns (Deployment memory deployment) { string memory json = vm.readFile(string.concat("addresses/", Strings.toString(chainId()), ".json")); - deployment.fixedPriceSaleStrategy = json.readAddress(getKeyPrefix(FIXED_PRICE_SALE_STRATEGY)); - deployment.merkleMintSaleStrategy = json.readAddress(getKeyPrefix(MERKLE_MINT_SALE_STRATEGY)); - deployment.redeemMinterFactory = json.readAddress(getKeyPrefix(REDEEM_MINTER_FACTORY)); - deployment.contract1155Impl = json.readAddress(getKeyPrefix(CONTRACT_1155_IMPL)); - deployment.factoryImpl = json.readAddress(getKeyPrefix(FACTORY_IMPL)); - deployment.factoryProxy = json.readAddress(getKeyPrefix(FACTORY_PROXY)); - deployment.preminter = json.readAddress(getKeyPrefix(PREMINTER)); + deployment.fixedPriceSaleStrategy = readAddressOrDefaultToZero(json, FIXED_PRICE_SALE_STRATEGY); + deployment.merkleMintSaleStrategy = readAddressOrDefaultToZero(json, MERKLE_MINT_SALE_STRATEGY); + deployment.redeemMinterFactory = readAddressOrDefaultToZero(json, REDEEM_MINTER_FACTORY); + deployment.contract1155Impl = readAddressOrDefaultToZero(json, CONTRACT_1155_IMPL); + deployment.factoryImpl = readAddressOrDefaultToZero(json, FACTORY_IMPL); + deployment.factoryProxy = readAddressOrDefaultToZero(json, FACTORY_PROXY); + deployment.preminter = readAddressOrDefaultToZero(json, PREMINTER); } } diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index f23d941aa..e194e3ee8 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -168,10 +168,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { } function testTheForkPremint(string memory chainName) private { - if (keccak256(abi.encodePacked(chainName)) != keccak256(abi.encodePacked("zora_goerli"))) { - return; - } - console.log("testing on fork: ", chainName); // create and select the fork, which will be used for all subsequent calls @@ -180,7 +176,15 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { // 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); + address preminterAddress = getDeployment().preminter; + + if (preminterAddress == address(0)) { + console.log("preminter not configured for chain...skipping"); + return; + } + + preminter = ZoraCreator1155PremintExecutor(preminterAddress); + factoryImpl = ZoraCreator1155FactoryImpl(getDeployment().factoryImpl); console.log("building defaults"); @@ -198,8 +202,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { 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); diff --git a/yarn.lock b/yarn.lock index 3704e11da..c8df57d76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1823,9 +1823,9 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -"forge-std@https://github.com/foundry-rs/forge-std#cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653": - version "1.1.1" - resolved "https://github.com/foundry-rs/forge-std#cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653" +"forge-std@https://github.com/foundry-rs/forge-std#705263c95892a906d7af65f0f73ce8a4a0c80b80": + version "1.6.0" + resolved "https://github.com/foundry-rs/forge-std#705263c95892a906d7af65f0f73ce8a4a0c80b80" formdata-polyfill@^4.0.10: version "4.0.10" From 7d6fe39e34bbbf0a1f1f26b7ee7b608c052f5a8f Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 8 Sep 2023 10:20:26 -0700 Subject: [PATCH 17/18] wip on moving to an interface --- src/nft/ZoraCreator1155Impl.sol | 15 ++++--- src/premint/ZoraCreator1155Attribution.sol | 6 +-- .../ZoraCreator1155DelegatedCreation.sol | 42 +++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 src/premint/ZoraCreator1155DelegatedCreation.sol diff --git a/src/nft/ZoraCreator1155Impl.sol b/src/nft/ZoraCreator1155Impl.sol index e554f6641..d027df22d 100644 --- a/src/nft/ZoraCreator1155Impl.sol +++ b/src/nft/ZoraCreator1155Impl.sol @@ -31,7 +31,8 @@ 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"; +import {IZoraCreator1155DelegatedCreation} from "../premint/ZoraCreator1155DelegatedCreation.sol"; +import {PremintTokenSetup, PremintConfig} from "../premint/ZoraCreator1155Attribution.sol"; /// Imagine. Mint. Enjoy. /// @title ZoraCreator1155Impl @@ -67,14 +68,18 @@ contract ZoraCreator1155Impl is uint256 public constant PERMISSION_BIT_FUNDS_MANAGER = 2 ** 5; /// @notice Factory contract IFactoryManagedUpgradeGate internal immutable factory; + /// @notice Attribution contract + IZoraCreator1155DelegatedCreation internal immutable delegator; constructor( uint256 _mintFeeAmount, address _mintFeeRecipient, address _factory, - address _protocolRewards + address _protocolRewards, + IZoraCreator1155DelegatedCreation _delegator ) MintFeeManager(_mintFeeAmount, _mintFeeRecipient) ERC1155Rewards(_protocolRewards, _mintFeeRecipient) initializer { factory = IFactoryManagedUpgradeGate(_factory); + delegator = _delegator; } /// @notice Initializes the contract @@ -738,13 +743,13 @@ contract ZoraCreator1155Impl is return delegatedTokenId[premintConfig.uid]; } - bytes32 hashedPremintConfig = ZoraCreator1155Attribution.validateAndHashPremint(premintConfig); + bytes32 hashedPremintConfig = delegator.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); + emit CreatorAttribution(hashedPremintConfig, delegator.HASHED_NAME(), delegator.HASHED_VERSION(), signature); // recover the signer from the data - address recoveredSigner = ZoraCreator1155Attribution.recoverSignerHashed(hashedPremintConfig, signature, address(this), block.chainid); + address recoveredSigner = delegator.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); diff --git a/src/premint/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol index 5efab4f8b..05f974e7d 100644 --- a/src/premint/ZoraCreator1155Attribution.sol +++ b/src/premint/ZoraCreator1155Attribution.sol @@ -94,7 +94,7 @@ library ZoraCreator1155Attribution { bytes calldata signature, address erc1155Contract, uint256 chainId - ) public pure returns (address signatory) { + ) internal pure returns (address signatory) { // first validate the signature - the creator must match the signer of the message bytes32 digest = _hashTypedDataV4( hashedPremintConfig, @@ -111,7 +111,7 @@ library ZoraCreator1155Attribution { /// 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) { + function premintHashedTypeDataV4(PremintConfig calldata premintConfig, address erc1155Contract, uint256 chainId) internal pure returns (bytes32) { // build the struct hash to be signed // here we pass the chain id, allowing the message to be signed for another chain return _hashTypedDataV4(hashPremint(premintConfig), erc1155Contract, chainId); @@ -159,7 +159,7 @@ library ZoraCreator1155Attribution { error MintNotYetStarted(); error PremintDeleted(); - function validateAndHashPremint(PremintConfig calldata premintConfig) external view returns (bytes32) { + function validateAndHashPremint(PremintConfig calldata premintConfig) internal 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(); diff --git a/src/premint/ZoraCreator1155DelegatedCreation.sol b/src/premint/ZoraCreator1155DelegatedCreation.sol new file mode 100644 index 000000000..8b87cdcc1 --- /dev/null +++ b/src/premint/ZoraCreator1155DelegatedCreation.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {PremintConfig, ZoraCreator1155Attribution} from "./ZoraCreator1155Attribution.sol"; + +interface IZoraCreator1155DelegatedCreation { + function validateAndHashPremint(PremintConfig calldata premintConfig) external view returns (bytes32); + + function recoverSignerHashed( + bytes32 hashedPremintConfig, + bytes calldata signature, + address erc1155Contract, + uint256 chainId + ) external pure returns (address signatory); + + function HASHED_NAME() external returns (bytes32); + + function HASHED_VERSION() external returns (bytes32); +} + +contract ZoraCreator1155DelegatedCreation is IZoraCreator1155DelegatedCreation { + function validateAndHashPremint(PremintConfig calldata premintConfig) external view returns (bytes32) { + return ZoraCreator1155Attribution.validateAndHashPremint(premintConfig); + } + + function recoverSignerHashed( + bytes32 hashedPremintConfig, + bytes calldata signature, + address erc1155Contract, + uint256 chainId + ) external pure returns (address signatory) { + return ZoraCreator1155Attribution.recoverSignerHashed(hashedPremintConfig, signature, erc1155Contract, chainId); + } + + function HASHED_NAME() external pure override returns (bytes32) { + return ZoraCreator1155Attribution.HASHED_NAME; + } + + function HASHED_VERSION() external pure override returns (bytes32) { + return ZoraCreator1155Attribution.HASHED_VERSION; + } +} From 1be93bdb4a0b8f01c87f69e22e9d8d4cc20d3e5e Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Mon, 11 Sep 2023 15:26:07 -0700 Subject: [PATCH 18/18] Updated tests to use new constructor arguments. It seems that the contract size has increased beyond the limit with this change. --- script/Deploy.s.sol | 11 ++++++-- script/DeployPreminter.s.sol | 4 ++- script/TestCreateDeterministic.sol | 9 ++++++- script/Upgrade.s.sol | 11 +++++++- src/premint/ZoraCreator1155Attribution.sol | 2 +- .../ZoraCreator1155DelegatedCreation.sol | 6 +++++ test/factory/ZoraCreator1155Factory.t.sol | 25 ++++++++++++++++--- test/fee/MintFeeManager.t.sol | 5 ++-- .../ZoraCreatorFixedPriceSaleStrategy.t.sol | 3 ++- .../ZoraCreatorMerkleMinterStrategy.t.sol | 3 ++- .../ZoraCreatorRedeemMinterFactory.t.sol | 3 ++- .../ZoraCreatorRedeemMinterStrategy.t.sol | 3 ++- test/nft/ZoraCreator1155.t.sol | 7 +++--- ...oraCreator1155AccessControlGeneralTest.sol | 3 ++- test/premint/ZoraCreator1155Preminter.t.sol | 19 +++++++++----- test/royalties/CreatorRoyaltiesControl.t.sol | 5 ++-- 16 files changed, 92 insertions(+), 27 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index b302012b5..0da821938 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -18,6 +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 {ZoraCreator1155DelegatedCreation} from "../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract DeployScript is ZoraDeployerBase { function run() public returns (string memory) { @@ -36,6 +37,7 @@ contract DeployScript is ZoraDeployerBase { ZoraCreatorFixedPriceSaleStrategy fixedPricedMinter = new ZoraCreatorFixedPriceSaleStrategy(); ZoraCreatorMerkleMinterStrategy merkleMinter = new ZoraCreatorMerkleMinterStrategy(); ZoraCreatorRedeemMinterFactory redeemMinterFactory = new ZoraCreatorRedeemMinterFactory(); + ZoraCreator1155DelegatedCreation delegatedCreation = new ZoraCreator1155DelegatedCreation(); deployment.fixedPriceSaleStrategy = address(fixedPricedMinter); deployment.merkleMintSaleStrategy = address(merkleMinter); @@ -46,8 +48,13 @@ contract DeployScript is ZoraDeployerBase { deployment.factoryProxy = address(factoryProxy); - ZoraCreator1155Impl creatorImpl = - new ZoraCreator1155Impl(chainConfig.mintFeeAmount, chainConfig.mintFeeRecipient, address(factoryProxy), chainConfig.protocolRewards); + ZoraCreator1155Impl creatorImpl = new ZoraCreator1155Impl( + chainConfig.mintFeeAmount, + chainConfig.mintFeeRecipient, + address(factoryProxy), + chainConfig.protocolRewards, + delegatedCreation + ); deployment.contract1155Impl = address(creatorImpl); diff --git a/script/DeployPreminter.s.sol b/script/DeployPreminter.s.sol index 4126fd9b1..ecf88ab2e 100644 --- a/script/DeployPreminter.s.sol +++ b/script/DeployPreminter.s.sol @@ -19,6 +19,7 @@ import {ZoraCreatorFixedPriceSaleStrategy} from "../src/minters/fixed-price/Zora import {ZoraCreatorMerkleMinterStrategy} from "../src/minters/merkle/ZoraCreatorMerkleMinterStrategy.sol"; import {ZoraCreatorRedeemMinterFactory} from "../src/minters/redeem/ZoraCreatorRedeemMinterFactory.sol"; import {ZoraCreator1155PremintExecutor} from "../src/premint/ZoraCreator1155PremintExecutor.sol"; +import {ZoraCreator1155DelegatedCreation} from "../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract DeployPreminter is ZoraDeployerBase { function run() public returns (string memory) { @@ -45,7 +46,8 @@ contract DeployPreminter is ZoraDeployerBase { chainConfig.mintFeeAmount, chainConfig.mintFeeRecipient, address(factoryProxy), - chainConfig.protocolRewards + chainConfig.protocolRewards, + new ZoraCreator1155DelegatedCreation() ); deployment.contract1155Impl = address(creatorImpl); diff --git a/script/TestCreateDeterministic.sol b/script/TestCreateDeterministic.sol index 28ae44a4d..e5f04b9e7 100644 --- a/script/TestCreateDeterministic.sol +++ b/script/TestCreateDeterministic.sol @@ -19,6 +19,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 {ZoraCreator1155DelegatedCreation} from "../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract DeployScript is ZoraDeployerBase { function run() public { @@ -43,7 +44,13 @@ contract DeployScript is ZoraDeployerBase { vm.startBroadcast(deployerPrivateKey); - ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(0, address(0), address(0), address(new ProtocolRewards())); + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl( + 0, + address(0), + address(0), + address(new ProtocolRewards()), + new ZoraCreator1155DelegatedCreation() + ); // get above constructor args encoded for verification later: ZoraCreator1155FactoryImpl factory = new ZoraCreator1155FactoryImpl( zoraCreator1155Impl, diff --git a/script/Upgrade.s.sol b/script/Upgrade.s.sol index a085a2c55..d52ce2883 100644 --- a/script/Upgrade.s.sol +++ b/script/Upgrade.s.sol @@ -17,6 +17,7 @@ import {ZoraCreatorFixedPriceSaleStrategy} from "../src/minters/fixed-price/Zora import {ZoraCreatorMerkleMinterStrategy} from "../src/minters/merkle/ZoraCreatorMerkleMinterStrategy.sol"; import {ZoraCreatorRedeemMinterFactory} from "../src/minters/redeem/ZoraCreatorRedeemMinterFactory.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ZoraCreator1155DelegatedCreation} from "../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract UpgradeScript is ZoraDeployerBase { using Strings for uint256; @@ -56,7 +57,15 @@ contract UpgradeScript is ZoraDeployerBase { console2.log("mintFeeAmount", chainConfig.mintFeeAmount); console2.log("minFeeRecipient", chainConfig.mintFeeRecipient); console2.log("protocolRewards", chainConfig.protocolRewards); - deployment.contract1155Impl = address(new ZoraCreator1155Impl(chainConfig.mintFeeAmount, chainConfig.mintFeeRecipient, deployment.factoryProxy, chainConfig.protocolRewards)); + deployment.contract1155Impl = address( + new ZoraCreator1155Impl( + chainConfig.mintFeeAmount, + chainConfig.mintFeeRecipient, + deployment.factoryProxy, + chainConfig.protocolRewards, + new ZoraCreator1155DelegatedCreation() + ) + ); console2.log("New NFT_IMPL", deployment.contract1155Impl); } else { console2.log("Existing NFT_IMPL", deployment.contract1155Impl); diff --git a/src/premint/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol index 05f974e7d..7e2f7b232 100644 --- a/src/premint/ZoraCreator1155Attribution.sol +++ b/src/premint/ZoraCreator1155Attribution.sol @@ -73,7 +73,7 @@ library ZoraCreator1155Attribution { return keccak256(abi.encode(TYPE_HASH, nameHash, versionHash, chainId, verifyingContract)); } - function _hashTypedDataV4(bytes32 structHash, address verifyingContract, uint256 chainId) private pure returns (bytes32) { + function _hashTypedDataV4(bytes32 structHash, address verifyingContract, uint256 chainId) internal pure returns (bytes32) { return ECDSAUpgradeable.toTypedDataHash(_domainSeparatorV4(chainId, verifyingContract), structHash); } diff --git a/src/premint/ZoraCreator1155DelegatedCreation.sol b/src/premint/ZoraCreator1155DelegatedCreation.sol index 8b87cdcc1..c82817fb9 100644 --- a/src/premint/ZoraCreator1155DelegatedCreation.sol +++ b/src/premint/ZoraCreator1155DelegatedCreation.sol @@ -13,6 +13,8 @@ interface IZoraCreator1155DelegatedCreation { uint256 chainId ) external pure returns (address signatory); + function premintHashedTypeDataV4(PremintConfig calldata premintConfig, address erc1155Contract, uint256 chainId) external pure returns (bytes32); + function HASHED_NAME() external returns (bytes32); function HASHED_VERSION() external returns (bytes32); @@ -39,4 +41,8 @@ contract ZoraCreator1155DelegatedCreation is IZoraCreator1155DelegatedCreation { function HASHED_VERSION() external pure override returns (bytes32) { return ZoraCreator1155Attribution.HASHED_VERSION; } + + function premintHashedTypeDataV4(PremintConfig calldata premintConfig, address erc1155Contract, uint256 chainId) external pure returns (bytes32) { + return ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, erc1155Contract, chainId); + } } diff --git a/test/factory/ZoraCreator1155Factory.t.sol b/test/factory/ZoraCreator1155Factory.t.sol index 8e79b0584..cb9a56b24 100644 --- a/test/factory/ZoraCreator1155Factory.t.sol +++ b/test/factory/ZoraCreator1155Factory.t.sol @@ -13,6 +13,7 @@ import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesCo import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {MockContractMetadata} from "../mock/MockContractMetadata.sol"; import {ProxyShim} from "../../src/utils/ProxyShim.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreator1155FactoryTest is Test { address internal zora; @@ -29,7 +30,13 @@ contract ZoraCreator1155FactoryTest is Test { Zora1155Factory factoryProxy = new Zora1155Factory(factoryShimAddress, ""); ProtocolRewards protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(protocolRewards)); + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl( + mintFeeAmount, + zora, + address(factoryProxy), + address(protocolRewards), + new ZoraCreator1155DelegatedCreation() + ); factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), IMinter1155(address(2)), IMinter1155(address(3))); factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); @@ -194,7 +201,13 @@ contract ZoraCreator1155FactoryTest is Test { // * create a new factory that points to that new erc1155 implementation, // * upgrade the proxy to point to the new factory uint256 newMintFeeAmount = 0.000888 ether; - IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, zora, address(factory), address(new ProtocolRewards())); + IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl( + newMintFeeAmount, + zora, + address(factory), + address(new ProtocolRewards()), + new ZoraCreator1155DelegatedCreation() + ); ZoraCreator1155FactoryImpl newFactoryImpl = new ZoraCreator1155FactoryImpl( newZoraCreator, @@ -246,7 +259,13 @@ contract ZoraCreator1155FactoryTest is Test { // 2. upgrade the created contract by creating a new contract and upgrading the existing one to point to it. uint256 newMintFeeAmount = 0.000888 ether; - IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl(newMintFeeAmount, zora, address(0), address(new ProtocolRewards())); + IZoraCreator1155 newZoraCreator = new ZoraCreator1155Impl( + newMintFeeAmount, + zora, + address(0), + address(new ProtocolRewards()), + new ZoraCreator1155DelegatedCreation() + ); address[] memory baseImpls = new address[](1); baseImpls[0] = address(factory.implementation()); diff --git a/test/fee/MintFeeManager.t.sol b/test/fee/MintFeeManager.t.sol index 12c903e5d..68fa2fd67 100644 --- a/test/fee/MintFeeManager.t.sol +++ b/test/fee/MintFeeManager.t.sol @@ -11,6 +11,7 @@ import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesCo import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Factory.sol"; import {IMintFeeManager} from "../../src/interfaces/IMintFeeManager.sol"; import {SimpleMinter} from "../mock/SimpleMinter.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract MintFeeManagerTest is Test { ZoraCreator1155Impl internal zoraCreator1155Impl; @@ -36,7 +37,7 @@ contract MintFeeManagerTest is Test { vm.assume(quantity < 100); vm.assume(mintFee < 0.1 ether); uint256 mintPrice = mintFee * quantity; - zoraCreator1155Impl = new ZoraCreator1155Impl(mintFee, recipient, address(0), address(protocolRewards)); + zoraCreator1155Impl = new ZoraCreator1155Impl(mintFee, recipient, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); target = ZoraCreator1155Impl(address(new Zora1155(address(zoraCreator1155Impl)))); adminRole = target.PERMISSION_BIT_ADMIN(); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, _emptyInitData()); @@ -70,7 +71,7 @@ contract MintFeeManagerTest is Test { _recip.setReceiveETH(false); address _recipient = address(_recip); - zoraCreator1155Impl = new ZoraCreator1155Impl(mintFee, _recipient, address(0), address(protocolRewards)); + zoraCreator1155Impl = new ZoraCreator1155Impl(mintFee, _recipient, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); target = ZoraCreator1155Impl(address(new Zora1155(address(zoraCreator1155Impl)))); adminRole = target.PERMISSION_BIT_ADMIN(); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, _emptyInitData()); diff --git a/test/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.t.sol b/test/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.t.sol index caa87907b..ed4c76c6f 100644 --- a/test/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.t.sol +++ b/test/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.t.sol @@ -11,6 +11,7 @@ import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltie import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; import {ILimitedMintPerAddress} from "../../../src/interfaces/ILimitedMintPerAddress.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreatorFixedPriceSaleStrategyTest is Test { ZoraCreator1155Impl internal target; @@ -25,7 +26,7 @@ contract ZoraCreatorFixedPriceSaleStrategyTest is Test { zora = makeAddr("zora"); bytes[] memory emptyData = new bytes[](0); ProtocolRewards protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); + ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); Zora1155 proxy = new Zora1155(address(targetImpl)); target = ZoraCreator1155Impl(address(proxy)); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, emptyData); diff --git a/test/minters/merkle/ZoraCreatorMerkleMinterStrategy.t.sol b/test/minters/merkle/ZoraCreatorMerkleMinterStrategy.t.sol index 60c4c7781..49db5921a 100644 --- a/test/minters/merkle/ZoraCreatorMerkleMinterStrategy.t.sol +++ b/test/minters/merkle/ZoraCreatorMerkleMinterStrategy.t.sol @@ -11,6 +11,7 @@ import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltie import {ILimitedMintPerAddress} from "../../../src/interfaces/ILimitedMintPerAddress.sol"; import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; import {ZoraCreatorMerkleMinterStrategy} from "../../../src/minters/merkle/ZoraCreatorMerkleMinterStrategy.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreatorMerkleMinterStrategyTest is Test { ProtocolRewards internal protocolRewards; @@ -25,7 +26,7 @@ contract ZoraCreatorMerkleMinterStrategyTest is Test { zora = makeAddr("zora"); bytes[] memory emptyData = new bytes[](0); protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); + ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); Zora1155 proxy = new Zora1155(address(targetImpl)); target = ZoraCreator1155Impl(address(proxy)); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, emptyData); diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol index 92a9f26fb..a7b2ea31f 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol @@ -15,6 +15,7 @@ import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltie import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; import {ZoraCreatorRedeemMinterStrategy} from "../../../src/minters/redeem/ZoraCreatorRedeemMinterStrategy.sol"; import {ZoraCreatorRedeemMinterFactory} from "../../../src/minters/redeem/ZoraCreatorRedeemMinterFactory.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreatorRedeemMinterFactoryTest is Test { ProtocolRewards internal protocolRewards; @@ -30,7 +31,7 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test { zora = makeAddr("zora"); bytes[] memory emptyData = new bytes[](0); protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); + ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); Zora1155 proxy = new Zora1155(address(targetImpl)); target = ZoraCreator1155Impl(address(proxy)); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), tokenAdmin, emptyData); diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol index 3be42ba5b..53aa6dbae 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol @@ -13,6 +13,7 @@ import {IRenderer1155} from "../../../src/interfaces/IRenderer1155.sol"; import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; import {ZoraCreatorRedeemMinterStrategy} from "../../../src/minters/redeem/ZoraCreatorRedeemMinterStrategy.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreatorRedeemMinterStrategyTest is Test { ProtocolRewards internal protocolRewards; @@ -30,7 +31,7 @@ contract ZoraCreatorRedeemMinterStrategyTest is Test { zora = makeAddr("zora"); bytes[] memory emptyData = new bytes[](0); protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); + ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); Zora1155 proxy = new Zora1155(address(targetImpl)); target = ZoraCreator1155Impl(address(proxy)); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, emptyData); diff --git a/test/nft/ZoraCreator1155.t.sol b/test/nft/ZoraCreator1155.t.sol index 26f2538d6..54d3cf0ea 100644 --- a/test/nft/ZoraCreator1155.t.sol +++ b/test/nft/ZoraCreator1155.t.sol @@ -16,6 +16,7 @@ import {IZoraCreator1155TypesV1} from "../../src/nft/IZoraCreator1155TypesV1.sol import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Factory.sol"; import {ICreatorRendererControl} from "../../src/interfaces/ICreatorRendererControl.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../src/premint/ZoraCreator1155DelegatedCreation.sol"; import {SimpleMinter} from "../mock/SimpleMinter.sol"; import {SimpleRenderer} from "../mock/SimpleRenderer.sol"; @@ -57,7 +58,7 @@ contract ZoraCreator1155Test is Test { protocolRewards = new ProtocolRewards(); upgradeGate = new MockUpgradeGate(); upgradeGate.initialize(admin); - zoraCreator1155Impl = new ZoraCreator1155Impl(0, zora, address(upgradeGate), address(protocolRewards)); + zoraCreator1155Impl = new ZoraCreator1155Impl(0, zora, address(upgradeGate), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); target = ZoraCreator1155Impl(address(new Zora1155(address(zoraCreator1155Impl)))); simpleMinter = new SimpleMinter(); fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); @@ -1197,7 +1198,7 @@ contract ZoraCreator1155Test is Test { } function test_unauthorizedUpgradeFails() external { - address new1155Impl = address(new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards))); + address new1155Impl = address(new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation())); vm.expectRevert(); target.upgradeTo(new1155Impl); @@ -1209,7 +1210,7 @@ contract ZoraCreator1155Test is Test { oldImpls[0] = address(zoraCreator1155Impl); - address new1155Impl = address(new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards))); + address new1155Impl = address(new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation())); vm.prank(upgradeGate.owner()); upgradeGate.registerUpgradePath(oldImpls, new1155Impl); diff --git a/test/nft/ZoraCreator1155AccessControlGeneralTest.sol b/test/nft/ZoraCreator1155AccessControlGeneralTest.sol index 070cc2da8..1645cb252 100644 --- a/test/nft/ZoraCreator1155AccessControlGeneralTest.sol +++ b/test/nft/ZoraCreator1155AccessControlGeneralTest.sol @@ -14,6 +14,7 @@ import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Fact import {ICreatorRendererControl} from "../../src/interfaces/ICreatorRendererControl.sol"; import {SimpleMinter} from "../mock/SimpleMinter.sol"; import {SimpleRenderer} from "../mock/SimpleRenderer.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreator1155AccessControlGeneralTest is Test { ProtocolRewards internal protocolRewards; @@ -25,7 +26,7 @@ contract ZoraCreator1155AccessControlGeneralTest is Test { function setUp() external { zora = makeAddr("zora"); protocolRewards = new ProtocolRewards(); - zoraCreator1155Impl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); + zoraCreator1155Impl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); target = ZoraCreator1155Impl(address(new Zora1155(address(zoraCreator1155Impl)))); admin = payable(address(0x9)); target.initialize("", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, _emptyInitData()); diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index e194e3ee8..ceab49cb1 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -21,6 +21,7 @@ import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; import {ZoraCreator1155Attribution, ContractCreationConfig, TokenCreationConfig, PremintConfig} from "../../src/premint/ZoraCreator1155Attribution.sol"; import {ForkDeploymentConfig} from "../../src/deployment/DeploymentConfig.sol"; import {ProxyShim} from "../../src/utils/ProxyShim.sol"; +import {ZoraCreator1155DelegatedCreation, IZoraCreator1155DelegatedCreation} from "../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { uint256 internal constant CONTRACT_BASE_ID = 0; @@ -39,6 +40,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { address internal zora; address internal premintExecutor; address internal collector; + IZoraCreator1155DelegatedCreation delegatedCreation; event Preminted( address indexed contractAddress, @@ -62,7 +64,8 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { address factoryShimAddress = address(new ProxyShim(zora)); Zora1155Factory factoryProxy = new Zora1155Factory(factoryShimAddress, ""); ProtocolRewards rewards = new ProtocolRewards(); - ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(rewards)); + delegatedCreation = new ZoraCreator1155DelegatedCreation(); + ZoraCreator1155Impl zoraCreator1155Impl = new ZoraCreator1155Impl(mintFeeAmount, zora, address(factoryProxy), address(rewards), delegatedCreation); ZoraCreatorFixedPriceSaleStrategy fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); @@ -117,7 +120,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { address contractAddress = preminter.getContractAddress(contractConfig); // 2. Call smart contract to get digest to sign for creation params. - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + bytes32 digest = delegatedCreation.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); // 3. Sign the digest // create a signature with the digest for the params @@ -142,7 +145,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { premintConfig.tokenConfig.tokenURI = "blah2.token"; premintConfig.uid++; - digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + digest = delegatedCreation.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); signature = _sign(creatorPrivateKey, digest); vm.deal(premintExecutor, mintCost); @@ -203,7 +206,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { address contractAddress = preminter.getContractAddress(contractConfig); // 2. Call smart contract to get digest to sign for creation params. - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + bytes32 digest = delegatedCreation.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); // 3. Sign the digest // create a signature with the digest for the params @@ -657,6 +660,10 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "yo"); } + // function premintHashedTypeDataV4(PremintConfig calldata premintConfig, address erc1155Contract, uint256 chainId) public pure returns (bytes32) { + // return ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, erc1155Contract, chainId); + // } + function _signAndExecutePremint( ContractCreationConfig memory contractConfig, PremintConfig memory premintConfig, @@ -681,8 +688,8 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { PremintConfig memory premintConfig, uint256 privateKey, uint256 chainId - ) private pure returns (bytes memory) { - bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); + ) private view returns (bytes memory) { + bytes32 digest = delegatedCreation.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); // 3. Sign the digest // create a signature with the digest for the params diff --git a/test/royalties/CreatorRoyaltiesControl.t.sol b/test/royalties/CreatorRoyaltiesControl.t.sol index 6d0fc7b34..881449c62 100644 --- a/test/royalties/CreatorRoyaltiesControl.t.sol +++ b/test/royalties/CreatorRoyaltiesControl.t.sol @@ -11,6 +11,7 @@ import {ICreatorRoyaltiesControl} from "../../src/interfaces/ICreatorRoyaltiesCo import {IZoraCreator1155Factory} from "../../src/interfaces/IZoraCreator1155Factory.sol"; import {IMintFeeManager} from "../../src/interfaces/IMintFeeManager.sol"; import {SimpleMinter} from "../mock/SimpleMinter.sol"; +import {ZoraCreator1155DelegatedCreation} from "../../src/premint/ZoraCreator1155DelegatedCreation.sol"; contract CreatorRoyaltiesControlTest is Test { ProtocolRewards internal protocolRewards; @@ -35,7 +36,7 @@ contract CreatorRoyaltiesControlTest is Test { function test_GetsRoyaltiesInfoGlobalDefault() external { address royaltyPayout = address(0x999); - zoraCreator1155Impl = new ZoraCreator1155Impl(0, recipient, address(0), address(protocolRewards)); + zoraCreator1155Impl = new ZoraCreator1155Impl(0, recipient, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); target = ZoraCreator1155Impl(address(new Zora1155(address(zoraCreator1155Impl)))); adminRole = target.PERMISSION_BIT_ADMIN(); target.initialize("", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(10, 10, address(royaltyPayout)), admin, _emptyInitData()); @@ -52,7 +53,7 @@ contract CreatorRoyaltiesControlTest is Test { function test_GetsRoyaltiesInfoSpecificToken() external { address royaltyPayout = address(0x999); - zoraCreator1155Impl = new ZoraCreator1155Impl(0, recipient, address(0), address(protocolRewards)); + zoraCreator1155Impl = new ZoraCreator1155Impl(0, recipient, address(0), address(protocolRewards), new ZoraCreator1155DelegatedCreation()); target = ZoraCreator1155Impl(address(new Zora1155(address(zoraCreator1155Impl)))); adminRole = target.PERMISSION_BIT_ADMIN(); target.initialize("", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(100, 10, address(royaltyPayout)), admin, _emptyInitData());