From 43ffa60c7fbbdfd7c9cf0833b518a72dc2309d4f Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 17 Nov 2023 14:48:23 -0800 Subject: [PATCH] Changed premint executor to use simpler method for determining if signer is authorized to sign a premint (#344) * Simplified authorization determining methods for premint; Instead of having a bunch of methods that need to decode a signature and determine if a creator is authorized to create premints, just have a single method that determines if the creator is authorized, and put the burden on external clients to decode the signature. * added new premint sdk method to replace existing `isAuthorizedToSign` function that existed on the contract` --- .changeset/violet-starfishes-visit.md | 13 +- .changeset/wicked-dolphins-remain.md | 5 + .../delegation/ZoraCreator1155Attribution.sol | 24 ++-- .../ZoraCreator1155PremintExecutorImpl.sol | 65 +++------ .../IZoraCreator1155PremintExecutor.sol | 20 +-- .../ZoraCreator1155PremintExecutor.t.sol | 33 +++-- packages/protocol-sdk/src/preminter.ts | 136 ++++++++++++++++++ 7 files changed, 205 insertions(+), 91 deletions(-) create mode 100644 .changeset/wicked-dolphins-remain.md create mode 100644 packages/protocol-sdk/src/preminter.ts diff --git a/.changeset/violet-starfishes-visit.md b/.changeset/violet-starfishes-visit.md index bdd1b3c6c..808aa7a11 100644 --- a/.changeset/violet-starfishes-visit.md +++ b/.changeset/violet-starfishes-visit.md @@ -109,6 +109,15 @@ struct TokenCreationConfig { * new function `premintV1` - takes a `PremintConfig`, and premint v1 signature, and executes a premint, with added functionality of being able to specify mint referral and mint recipient * new function `premintV2` - takes a `PremintConfigV2` signature and executes a premint, with being able to specify mint referral and mint recipient * deprecated function `premint` - call `premintV1` instead -* new function `isValidSignatureV1` - takes an 1155 address, contract admin, premint v1 config and signature, and validates the signature. Can be used for 1155 contracts that were not created via the premint executor contract. -* new function `isValidSignatureV2` - takes an 1155 address, contract admin, premint v2 config and signature, and validates the signature. Can be used for 1155 contracts that were not created via the premint executor contract. +* new function + +```solidity +isAuthorizedToCreatePremint( + address signer, + address premintContractConfigContractAdmin, + address contractAddress +) public view returns (bool isAuthorized) +``` + +takes a signer, contractConfig.contractAdmin, and 1155 address, and determines if the signer is authorized to sign premints on the given contract. Replaces `isValidSignature` - by putting the burden on clients to first decode the signature, then pass the recovered signer to this function to determine if the signer has premint authorization on the contract. * deprecated function `isValidSignature` - call `isValidSignatureV1` instead diff --git a/.changeset/wicked-dolphins-remain.md b/.changeset/wicked-dolphins-remain.md new file mode 100644 index 000000000..d62c0c26c --- /dev/null +++ b/.changeset/wicked-dolphins-remain.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/premint-sdk": patch +--- + +`preminter` exposes new function isValidSignatureV1 that recovers a signer from a signed premint and determines if that signer is authorized to sign diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol index 6f24664b7..55a1b7cc6 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol @@ -234,25 +234,17 @@ library ZoraCreator1155Attribution { /// @dev copied from ZoraCreator1155Impl uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; - function isValidSignature( - address originalPremintCreator, - address contractAddress, - bytes32 structHash, - bytes32 hashedVersion, - bytes calldata signature - ) internal view returns (bool isValid, address recoveredSigner) { - recoveredSigner = recoverSignerHashed(structHash, signature, contractAddress, hashedVersion, block.chainid); - - if (recoveredSigner == address(0)) { - return (false, address(0)); - } - - // if contract hasn't been created, signer must be the contract admin on the config + function isAuthorizedToCreatePremint( + address signer, + address premintContractConfigContractAdmin, + address contractAddress + ) internal view returns (bool authorized) { + // if contract hasn't been created, signer must be the contract admin on the premint config if (contractAddress.code.length == 0) { - isValid = recoveredSigner == originalPremintCreator; + return signer == premintContractConfigContractAdmin; } else { // if contract has been created, signer must have mint new token permission - isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); + authorized = IZoraCreator1155(contractAddress).isAdminOrRole(signer, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); } } } diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol index 6f8332340..ec360e993 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol @@ -153,7 +153,7 @@ contract ZoraCreator1155PremintExecutorImpl is return (true, ERC1155DelegationStorageV1(contractAddress).delegatedTokenId(uid)); } - // @custom:deprecated use isValidSignatureV1 instead + // @custom:deprecated use isAuthorizedToCreatePremint instead function isValidSignature( ContractCreationConfig calldata contractConfig, PremintConfig calldata premintConfig, @@ -161,57 +161,34 @@ contract ZoraCreator1155PremintExecutorImpl is ) public view returns (bool isValid, address contractAddress, address recoveredSigner) { contractAddress = getContractAddress(contractConfig); - (isValid, recoveredSigner) = isValidSignatureV1(contractConfig.contractAdmin, contractAddress, premintConfig, signature); - } - - /// @notice Recovers the signer of a premint, and checks if the signer is authorized to sign the premint. - /// @dev for use with v1 of premint config, PremintConfig - /// @param premintContractConfigContractAdmin If this contract was created via premint, the original contractConfig.contractAdmin. Otherwise, set to address(0) - /// @param contractAddress The determinstic 1155 contract address the premint is for - /// @param premintConfig The premint config - /// @param signature The signature of the premint - /// @return isValid Whether the signature is valid - /// @return recoveredSigner The signer of the premint - function isValidSignatureV1( - address premintContractConfigContractAdmin, - address contractAddress, - PremintConfig calldata premintConfig, - bytes calldata signature - ) public view returns (bool isValid, address recoveredSigner) { - bytes32 hashedPremint = ZoraCreator1155Attribution.hashPremint(premintConfig); - - (isValid, recoveredSigner) = ZoraCreator1155Attribution.isValidSignature( - premintContractConfigContractAdmin, + recoveredSigner = ZoraCreator1155Attribution.recoverSignerHashed( + ZoraCreator1155Attribution.hashPremint(premintConfig), + signature, contractAddress, - hashedPremint, ZoraCreator1155Attribution.HASHED_VERSION_1, - signature + block.chainid ); + + if (recoveredSigner == address(0)) { + return (false, address(0), recoveredSigner); + } + + isValid = isAuthorizedToCreatePremint(recoveredSigner, contractConfig.contractAdmin, contractAddress); } - /// @notice Recovers the signer of a premint, and checks if the signer is authorized to sign the premint. - /// @dev for use with v2 of premint config, PremintConfig + /// @notice Checks if the signer of a premint is authorized to sign a premint for a given contract. If the contract hasn't been created yet, + /// then the signer is authorized if the signer's address matches contractConfig.contractAdmin. Otherwise, the signer must have the PERMISSION_BIT_MINTER + /// role on the contract + /// @param signer The signer of the premint /// @param premintContractConfigContractAdmin If this contract was created via premint, the original contractConfig.contractAdmin. Otherwise, set to address(0) /// @param contractAddress The determinstic 1155 contract address the premint is for - /// @param premintConfig The premint config - /// @param signature The signature of the premint - /// @return isValid Whether the signature is valid - /// @return recoveredSigner The signer of the premint - function isValidSignatureV2( + /// @return isAuthorized Whether the signer is authorized + function isAuthorizedToCreatePremint( + address signer, address premintContractConfigContractAdmin, - address contractAddress, - PremintConfigV2 calldata premintConfig, - bytes calldata signature - ) public view returns (bool isValid, address recoveredSigner) { - bytes32 hashedPremint = ZoraCreator1155Attribution.hashPremint(premintConfig); - - (isValid, recoveredSigner) = ZoraCreator1155Attribution.isValidSignature( - premintContractConfigContractAdmin, - contractAddress, - hashedPremint, - ZoraCreator1155Attribution.HASHED_VERSION_2, - signature - ); + address contractAddress + ) public view returns (bool isAuthorized) { + return ZoraCreator1155Attribution.isAuthorizedToCreatePremint(signer, premintContractConfigContractAdmin, contractAddress); } /// @notice Returns the versions of the premint signature that the contract supports diff --git a/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol b/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol index 8c4c92ef8..967166cfc 100644 --- a/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol +++ b/packages/1155-contracts/src/interfaces/IZoraCreator1155PremintExecutor.sol @@ -27,6 +27,12 @@ interface ILegacyZoraCreator1155PremintExecutor { uint256 quantityToMint, string calldata mintComment ) external payable returns (uint256 newTokenId); + + function isAuthorizedToCreatePremint( + address signer, + address premintContractConfigContractAdmin, + address contractAddress + ) external view returns (bool isAuthorized); } interface IZoraCreator1155PremintExecutorV1 { @@ -37,13 +43,6 @@ interface IZoraCreator1155PremintExecutorV1 { uint256 quantityToMint, IZoraCreator1155PremintExecutor.MintArguments calldata mintArguments ) external payable returns (IZoraCreator1155PremintExecutor.PremintResult memory); - - function isValidSignatureV1( - address originalContractAdmin, - address contractAddress, - PremintConfig calldata premintConfig, - bytes calldata signature - ) external view returns (bool isValid, address recoveredSigner); } interface IZoraCreator1155PremintExecutorV2 { @@ -54,13 +53,6 @@ interface IZoraCreator1155PremintExecutorV2 { uint256 quantityToMint, IZoraCreator1155PremintExecutor.MintArguments calldata mintArguments ) external payable returns (IZoraCreator1155PremintExecutor.PremintResult memory); - - function isValidSignatureV2( - address originalContractAdmin, - address contractAddress, - PremintConfigV2 calldata premintConfig, - bytes calldata signature - ) external view returns (bool isValid, address recoveredSigner); } interface IZoraCreator1155PremintExecutor is diff --git a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol index 11e15b0fa..9ff66fc4e 100644 --- a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol +++ b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -132,7 +132,7 @@ contract ZoraCreator1155PreminterTest is Test { assertTrue(isValid); // now check using new method - (isValid, ) = preminter.isValidSignatureV1(contractConfig.contractAdmin, contractAddress, premintConfig, signature); + isValid = preminter.isAuthorizedToCreatePremint(creator, contractConfig.contractAdmin, contractAddress); assertTrue(isValid); // now call the premint function, using the same config that was used to generate the digest, and the signature @@ -667,18 +667,13 @@ contract ZoraCreator1155PreminterTest is Test { address contractAddress = preminter.getContractAddress(contractConfig); - // sign and execute premint - bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, block.chainid); - - (bool isValidSignature, address recoveredSigner) = preminter.isValidSignatureV2( - contractConfig.contractAdmin, - contractAddress, - premintConfig, - signature - ); + bool isValidSignature = preminter.isAuthorizedToCreatePremint({ + signer: creator, + premintContractConfigContractAdmin: contractConfig.contractAdmin, + contractAddress: contractAddress + }); - assertEq(creator, recoveredSigner, "recovered the wrong signer"); - assertTrue(isValidSignature, "signature should be valid"); + assertTrue(isValidSignature, "creator should be allowed to create premint before contract created"); _signAndExecutePremint(contractConfig, premintConfig, creatorPrivateKey, block.chainid, premintExecutor, 1, "hi"); @@ -694,9 +689,13 @@ contract ZoraCreator1155PreminterTest is Test { bytes memory newCreatorSignature = _signPremint(contractAddress, premintConfig2, newCreatorPrivateKey, block.chainid); // it should not be considered a valid signature - (isValidSignature, ) = preminter.isValidSignatureV2(contractConfig.contractAdmin, contractAddress, premintConfig2, newCreatorSignature); + isValidSignature = preminter.isAuthorizedToCreatePremint({ + signer: newCreator, + premintContractConfigContractAdmin: contractConfig.contractAdmin, + contractAddress: contractAddress + }); - assertFalse(isValidSignature, "signature should not be valid"); + assertFalse(isValidSignature, "alternative creator should not be allowed to create a premint"); uint256 quantityToMint = 1; uint256 mintCost = mintFeeAmount * quantityToMint; @@ -712,7 +711,11 @@ contract ZoraCreator1155PreminterTest is Test { IZoraCreator1155(contractAddress).addPermission(CONTRACT_BASE_ID, newCreator, PERMISSION_BIT_MINTER); // should now be considered a valid signature - (isValidSignature, ) = preminter.isValidSignatureV2(contractConfig.contractAdmin, contractAddress, premintConfig2, newCreatorSignature); + isValidSignature = preminter.isAuthorizedToCreatePremint({ + signer: newCreator, + premintContractConfigContractAdmin: contractConfig.contractAdmin, + contractAddress: contractAddress + }); assertTrue(isValidSignature, "valid signature after granted permission"); vm.deal(premintExecutor, mintCost); diff --git a/packages/protocol-sdk/src/preminter.ts b/packages/protocol-sdk/src/preminter.ts new file mode 100644 index 000000000..ec46eb1c2 --- /dev/null +++ b/packages/protocol-sdk/src/preminter.ts @@ -0,0 +1,136 @@ +import { Address } from "abitype"; +import { ExtractAbiFunction, AbiParametersToPrimitiveTypes } from "abitype"; +import { + zoraCreator1155PremintExecutorImplABI as preminterAbi, + zoraCreator1155PremintExecutorImplAddress, +} from "@zoralabs/protocol-deployments"; +import { + TypedDataDefinition, + recoverTypedDataAddress, + Hex, + PublicClient, +} from "viem"; + +type PremintInputs = ExtractAbiFunction< + typeof preminterAbi, + "premint" +>["inputs"]; + +type PreminterHashDataTypes = AbiParametersToPrimitiveTypes; + +export type ContractCreationConfig = PreminterHashDataTypes[0]; +export type PremintConfig = PreminterHashDataTypes[1]; +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 { tokenConfig, uid, version, deleted } = premintConfig; + const types = { + 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" }, + ], + 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" }, + { name: "fixedPriceMinter", type: "address" }, + ], + }; + + const result: TypedDataDefinition = { + domain: { + chainId, + name: "Preminter", + version: "1", + verifyingContract: verifyingContract, + }, + types, + message: { + tokenConfig, + uid, + version, + deleted, + }, + primaryType: "CreatorAttribution", + }; + + return result; +}; + +export async function isValidSignatureV1({ + contractAddress, + originalContractAdmin, + premintConfig, + signature, + chainId, + publicClient, +}: { + contractAddress: Address; + originalContractAdmin: Address; + premintConfig: PremintConfig; + signature: Hex; + chainId: number; + publicClient: PublicClient; +}): Promise<{ + isAuthorized: boolean; + recoveredAddress?: Address; +}> { + const typedData = preminterTypedDataDefinition({ + verifyingContract: contractAddress, + premintConfig, + chainId, + }); + + // recover the address from the signature + let recoveredAddress: Address; + + try { + recoveredAddress = await recoverTypedDataAddress({ + ...typedData, + signature, + }); + } catch (error) { + console.error(error); + + return { + isAuthorized: false, + }; + } + + // premint executor is same address on all chains + const premintExecutorAddress = zoraCreator1155PremintExecutorImplAddress[999]; + + const isAuthorized = await publicClient.readContract({ + abi: preminterAbi, + address: premintExecutorAddress, + functionName: "isAuthorizedToCreatePremint", + args: [recoveredAddress, originalContractAdmin, contractAddress], + }); + + return { + isAuthorized, + recoveredAddress, + }; +}