From 2846489f40897767d2fd08db46e52ef6578aa5be Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 18 Aug 2023 12:20:27 -0700 Subject: [PATCH 1/3] Added methods to get status of creator attribution, and validate signatures, useful for the backend --- package/preminter.test.ts | 42 +++++----- src/interfaces/IZoraCreator1155.sol | 2 + .../ZoraCreator1155PremintExecutor.sol | 41 +++++++-- test/premint/ZoraCreator1155Preminter.t.sol | 83 ++++++++++++++++++- 4 files changed, 142 insertions(+), 26 deletions(-) diff --git a/package/preminter.test.ts b/package/preminter.test.ts index ade37ffc3..37d1d5da6 100644 --- a/package/preminter.test.ts +++ b/package/preminter.test.ts @@ -285,12 +285,7 @@ describe("ZoraCreator1155Preminter", () => { abi: preminterAbi, address: preminterAddress, functionName: "recoverSigner", - args: [ - premintConfig, - contractAddress, - signedMessage, - BigInt(anvilChainId), - ], + args: [premintConfig, contractAddress, signedMessage], }); expect(recoveredAddress).to.equal(creatorAccount); @@ -349,7 +344,16 @@ describe("ZoraCreator1155Preminter", () => { }); // get the premint status - it should not be minted - let tokenId: bigint; + let [contractCreated, tokenId] = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "premintStatus", + args: [contractAddress, premintConfig.uid], + }); + + expect(contractCreated).toBe(false); + expect(tokenId).toBe(0n); + // 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 @@ -378,21 +382,21 @@ describe("ZoraCreator1155Preminter", () => { expect(receipt.status).toBe("success"); // fetch the premint token id - let newTokenId = await publicClient.readContract({ - abi: zoraCreator1155ImplABI, - address: contractAddress, - functionName: "delegatedTokenId", - args: [premintConfig.uid], + [contractCreated, tokenId] = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + 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 @@ -447,11 +451,11 @@ describe("ZoraCreator1155Preminter", () => { ).toBe("success"); // now premint status for the second mint, it should be minted - tokenId = await publicClient.readContract({ - abi: zoraCreator1155ImplABI, - address: contractAddress, - functionName: "delegatedTokenId", - args: [premintConfig2.uid], + [, tokenId] = await publicClient.readContract({ + abi: preminterAbi, + address: preminterAddress, + functionName: "premintStatus", + args: [contractAddress, premintConfig2.uid], }); expect(tokenId).not.toBe(0n); diff --git a/src/interfaces/IZoraCreator1155.sol b/src/interfaces/IZoraCreator1155.sol index 7a663ac5a..d8b79e2b9 100644 --- a/src/interfaces/IZoraCreator1155.sol +++ b/src/interfaces/IZoraCreator1155.sol @@ -107,6 +107,8 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IVersionedContract, IOwna 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/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index 7b303a300..d7923d0b7 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -123,12 +123,41 @@ contract ZoraCreator1155PremintExecutor { return factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); } - function recoverSigner( + 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 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 mint new tokens on the erc1155 contract. + function isValidSignature( + ContractCreationConfig calldata contractConfig, PremintConfig calldata premintConfig, - address zor1155Address, - bytes calldata signature, - uint256 chainId - ) public pure returns (address) { - return ZoraCreator1155Attribution.recoverSigner(premintConfig, signature, zor1155Address, chainId); + 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/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index 71b6614a8..399b5985e 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -519,6 +519,87 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { 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( ContractCreationConfig memory contractConfig, PremintConfig memory premintConfig, @@ -540,7 +621,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { PremintConfig memory premintConfig, uint256 privateKey, uint256 chainId - ) private view returns (bytes memory) { + ) private pure returns (bytes memory) { bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, contractAddress, chainId); // 3. Sign the digest From e4a76b9f97736e263036cc0416d984dcd6598d19 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 18 Aug 2023 14:47:23 -0700 Subject: [PATCH 2/3] better comments --- .../ZoraCreator1155PremintExecutor.sol | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index d7923d0b7..a088bab32 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -11,10 +11,9 @@ import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCrea import {IMinter1155} from "../interfaces/IMinter1155.sol"; import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1155Attribution} from "./ZoraCreator1155Attribution.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. +/// @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; @@ -27,7 +26,6 @@ contract ZoraCreator1155PremintExecutor { error MintNotYetStarted(); error InvalidSignature(); - // todo: make a constructor constructor(IZoraCreator1155Factory _factory) { factory = _factory; } @@ -43,9 +41,11 @@ contract ZoraCreator1155PremintExecutor { 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. + /// 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. function premint( ContractCreationConfig calldata contractConfig, PremintConfig calldata premintConfig, @@ -53,27 +53,14 @@ contract ZoraCreator1155PremintExecutor { uint256 quantityToMint, string calldata mintComment ) public payable returns (uint256 newTokenId) { - // 1. Validate the signature. - // 2. get or create an erc1155 contract with the same determinsitic address as that from the contract config - // 3. Have the erc1155 contract create a new token. the signer must have permission to do mint new tokens - // (that role enforcement is expected to be in the tokenContract). - // 4. The erc1155 will sedtup the token with the signign address as the creator, and follow the creator rewards standard. - // 5. Mint x tokens, as configured, to the executor of this transaction. - // 6. Future: First minter gets rewards - // get or create the contract with the given params + // contract address is deterministic. (IZoraCreator1155 tokenContract, bool isNewContract) = _getOrCreateContract(contractConfig); address contractAddress = address(tokenContract); - // have the address setup the new token. The signer must have permission to do this. - // (that role enforcement is expected to be in the tokenContract). - // the token contract will: - - // * setup the token with the signer as the creator, and follow the creator rewards standard. - // * will revert if the token in the contract with the same uid already exists. - // * will make sure creator has admin rights to the token. - // * setup the token with the given token config. - // * return the new token id. + // 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. @@ -119,10 +106,14 @@ contract ZoraCreator1155PremintExecutor { 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); } @@ -137,9 +128,9 @@ contract ZoraCreator1155PremintExecutor { } /// @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 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 mint new tokens on the erc1155 contract. + /// 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, From 66609b6ffcce377e6d1d0157197a0ca83ac6e717 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 18 Aug 2023 15:11:46 -0700 Subject: [PATCH 3/3] fixed back fork test --- package/preminter.test.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/package/preminter.test.ts b/package/preminter.test.ts index 37d1d5da6..1c2b9b7bd 100644 --- a/package/preminter.test.ts +++ b/package/preminter.test.ts @@ -9,6 +9,7 @@ import { describe, it, beforeEach, expect } from "vitest"; import { parseEther } from "viem"; import { zoraCreator1155PremintExecutorABI as preminterAbi, + zoraCreator1155PremintExecutorAddress, zoraCreator1155ImplABI, zoraCreator1155FactoryImplAddress, zoraCreator1155FactoryImplConfig, @@ -211,15 +212,16 @@ describe("ZoraCreator1155Preminter", () => { ctx.preminterAddress = preminterAddress; }, 20 * 1000); - it( - "can sign for another chain", - async ({ preminterAddress: preminterAddress, fixedPriceMinterAddress }) => { + "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, @@ -239,14 +241,10 @@ describe("ZoraCreator1155Preminter", () => { console.log({ creatorAccount, signedMessage, + contractConfig, premintConfig, - contractAddress: await publicClient.readContract({ - abi: preminterAbi, - address: preminterAddress, - functionName: "getContractAddress", - args: [contractConfig], - }), - }); + contractAddress + }); }, 20 * 1000 ); @@ -293,7 +291,6 @@ describe("ZoraCreator1155Preminter", () => { 20 * 1000 ); - it( "can sign and mint multiple tokens", async ({ @@ -473,4 +470,5 @@ describe("ZoraCreator1155Preminter", () => { // 10 second timeout 40 * 1000 ); + });