From 7b8fa9a373c7e76873715672a0f16116286f42d8 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Mon, 18 Sep 2023 14:20:06 -0700 Subject: [PATCH 01/27] Added pre-release ci task From 1e6f9737f44ce0d70c041aebc5980a55b9f2b36b Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 31 May 2023 11:48:39 -0700 Subject: [PATCH 02/27] Gasless --- .changeset/long-avocados-visit.md | 4 +- .env.anvil | 3 +- script/DeployPreminter.s.sol | 42 ++ script/EstimatePreminterGas.s.sol | 90 ++++ src/premint/EIP712UpgradeableWithChainId.sol | 106 ++++ src/premint/README.md | 59 +++ src/premint/ZoraCreator1155Preminter.sol | 366 ++++++++++++++ test/premint/ZoraCreator1155Preminter.t.sol | 478 ++++++++++++++++++ uml/gasslessCreate-collecting-sequence.puml | 1 + .../gasslessCreate-collecting-sequence.svg | 2 +- .../gasslessCreate-creation-activity.svg | 2 +- .../gasslessCreate-creation-sequence.svg | 2 +- 12 files changed, 1150 insertions(+), 5 deletions(-) create mode 100644 script/DeployPreminter.s.sol create mode 100644 script/EstimatePreminterGas.s.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 diff --git a/.changeset/long-avocados-visit.md b/.changeset/long-avocados-visit.md index 581c4e430..953539c64 100644 --- a/.changeset/long-avocados-visit.md +++ b/.changeset/long-avocados-visit.md @@ -2,4 +2,6 @@ "@zoralabs/zora-1155-contracts": minor --- -Added deterministic contract creation from the Zora1155 factory +- Added deterministic contract creation from the Zora1155 factory +- Added gasless minter +- Added deterministic contract creation diff --git a/.env.anvil b/.env.anvil index 0c2078aa6..72e455548 100644 --- a/.env.anvil +++ b/.env.anvil @@ -1,2 +1,3 @@ FORK_RPC_URL="https://testnet.rpc.zora.co/" -FORK_BLOCK_NUMBER=916572 \ No newline at end of file +# FORK_BLOCK_NUMBER=916572 +FORK_BLOCK_NUMBER=700700 \ 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/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/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-sequence.puml b/uml/gasslessCreate-collecting-sequence.puml index 2eccc4c56..d6c017fdb 100644 --- a/uml/gasslessCreate-collecting-sequence.puml +++ b/uml/gasslessCreate-collecting-sequence.puml @@ -38,6 +38,7 @@ Group contract doesnt exist end PreminterContract -> 1155Contract: create new token\nwith signature +PreminterContract -> 1155Contract: set new token sale parameters PreminterContract -> 1155Contract: mint tokens to collector deactivate PreminterContract diff --git a/uml/generated/gasslessCreate-collecting-sequence.svg b/uml/generated/gasslessCreate-collecting-sequence.svg index 0115a8fed..8c3d4d2ad 100644 --- a/uml/generated/gasslessCreate-collecting-sequence.svg +++ b/uml/generated/gasslessCreate-collecting-sequence.svg @@ -1 +1 @@ -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 +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 diff --git a/uml/generated/gasslessCreate-creation-activity.svg b/uml/generated/gasslessCreate-creation-activity.svg index b0e6d67cc..dc92d51b9 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 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 +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  diff --git a/uml/generated/gasslessCreate-creation-sequence.svg b/uml/generated/gasslessCreate-creation-sequence.svg index 4d2fbdad9..f5005aaea 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 + 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 +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 From d00556fe18812c4dc9f2f612dbbdab4feea2f94d Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 22 Aug 2023 10:37:30 -0700 Subject: [PATCH 03/27] 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 | 2 +- .env.anvil | 2 +- package/preminter.test.ts | 1 + script/DeployPreminter.s.sol | 46 ++- script/EstimatePreminterGas.s.sol | 90 ----- src/interfaces/IZoraCreator1155.sol | 2 + src/premint/EIP712UpgradeableWithChainId.sol | 106 ----- src/premint/ZoraCreator1155Attribution.sol | 237 +++++++++++ .../ZoraCreator1155PremintExecutor.sol | 159 ++++++++ src/premint/ZoraCreator1155Preminter.sol | 366 ----------------- test/premint/ZoraCreator1155Preminter.t.sol | 377 +++++++++++++----- 11 files changed, 712 insertions(+), 676 deletions(-) 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 index e251c6a53..871e80dd9 100644 --- a/.changeset/twelve-comics-sniff.md +++ b/.changeset/twelve-comics-sniff.md @@ -2,4 +2,4 @@ "@zoralabs/zora-1155-contracts": minor --- -Added the PremintExecutor contract, and updated erc1155 to support delegated minting \ No newline at end of file +Added the PremintExecutor contract, and updated erc1155 to support delegated minting diff --git a/.env.anvil b/.env.anvil index 72e455548..4e183d130 100644 --- a/.env.anvil +++ b/.env.anvil @@ -1,3 +1,3 @@ FORK_RPC_URL="https://testnet.rpc.zora.co/" # FORK_BLOCK_NUMBER=916572 -FORK_BLOCK_NUMBER=700700 \ No newline at end of file +FORK_BLOCK_NUMBER=700700 diff --git a/package/preminter.test.ts b/package/preminter.test.ts index 100e46386..7c1c024c4 100644 --- a/package/preminter.test.ts +++ b/package/preminter.test.ts @@ -474,4 +474,5 @@ describe("ZoraCreator1155Preminter", () => { // 10 second timeout 40 * 1000 ); + }); 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 1488def9c..f571b835d 100644 --- a/src/interfaces/IZoraCreator1155.sol +++ b/src/interfaces/IZoraCreator1155.sol @@ -84,6 +84,8 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IZoraCreator1155Errors, I 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/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/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 From c54b05ed562079ae197e0322ed4dcba418904a09 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 23 Aug 2023 13:58:00 -0700 Subject: [PATCH 04/27] prettier fix --- package/preminter.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/package/preminter.test.ts b/package/preminter.test.ts index 7c1c024c4..100e46386 100644 --- a/package/preminter.test.ts +++ b/package/preminter.test.ts @@ -474,5 +474,4 @@ describe("ZoraCreator1155Preminter", () => { // 10 second timeout 40 * 1000 ); - }); From 08737a1024c6d5b52cfc62b26bba1477e161efa8 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Wed, 23 Aug 2023 12:58:37 -0700 Subject: [PATCH 05/27] 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 f2a8bbbb8a9fab5f3d1faafec2ee56d8143b7f2a Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 18 Aug 2023 12:30:35 -0700 Subject: [PATCH 06/27] deployed new creator attribution Created a release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a04e64b0b..58fa44067 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", From 012c3f19c402d81ba71d1f3a0dddbd94d2825bed Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Mon, 28 Aug 2023 17:05:31 -0400 Subject: [PATCH 07/27] fix: factory tests --- script/DeployScript.s.sol | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) 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)); + } +} From 9bccade5da381afd985d832f41c9d9f0c3cc8435 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 13:26:07 -0400 Subject: [PATCH 08/27] 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 4e1bfd23a7b489dc57ff4f1c23a7ae242fa6ec8f Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 18:54:40 -0400 Subject: [PATCH 09/27] 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 9aff1dc37f5a9c7af6a32672661a2d800ef865a5 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Tue, 29 Aug 2023 18:55:13 -0400 Subject: [PATCH 10/27] 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 e970353b87a8bd1283bdec27562c1f2d86903936 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Fri, 1 Sep 2023 13:40:06 -0400 Subject: [PATCH 11/27] 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 63b7a9ba30b8f71db0e57cc8c7f1040f1368af5e Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Fri, 1 Sep 2023 13:40:20 -0400 Subject: [PATCH 12/27] 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 89196675a6975d2f434003f09a89a63540e6ad77 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Mon, 11 Sep 2023 13:03:40 -0700 Subject: [PATCH 13/27] 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 --- test/premint/ZoraCreator1155Preminter.t.sol | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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); From 1b9984b29550ab4dcc0919045a54ff30b2a9d59c Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 12 Sep 2023 11:56:44 -0700 Subject: [PATCH 14/27] Premint: add creator to creator attribution event (#163) * Creator attribution - add creator to event * * Renamed creator attribution args from bytes32 to string to match the standard * Added test that verifies creator attribution event emitted --- src/premint/ZoraCreator1155Attribution.sol | 8 +++--- test/premint/ZoraCreator1155Preminter.t.sol | 30 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/premint/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol index 5efab4f8b..cc3d4bcf7 100644 --- a/src/premint/ZoraCreator1155Attribution.sol +++ b/src/premint/ZoraCreator1155Attribution.sol @@ -58,9 +58,11 @@ struct PremintConfig { /// @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)"); + string internal constant NAME = "Preminter"; + string internal constant VERSION = "1"; + bytes32 internal constant HASHED_NAME = keccak256(bytes(NAME)); + bytes32 internal constant HASHED_VERSION = keccak256(bytes(VERSION)); + bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); /** * @dev Returns the domain separator for the specified chain. diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index e194e3ee8..6d6868b06 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -156,6 +156,36 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), quantityToMint); } + event CreatorAttribution(bytes32 structHash, string domainName, string version, address creator, bytes signature); + + function test_premint_emitsCreatorAttribution_fromErc1155Contract() external { + // build a premint + ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); + PremintConfig memory premintConfig = makeDefaultPremintConfig(); + + // sign and execute premint + uint256 chainId = block.chainid; + + address deterministicAddress = preminter.getContractAddress(contractConfig); + bytes32 structHash = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, deterministicAddress, chainId); + bytes memory signature = _sign(creatorPrivateKey, structHash); + + uint256 quantityToMint = 4; + string memory comment = "hi"; + uint256 mintCost = mintFeeAmount * quantityToMint; + // this account will be used to execute the premint, and should result in a contract being created + vm.deal(collector, mintCost); + + vm.prank(collector); + + // verify CreatorAttribution was emitted from the erc1155 contract + vm.expectEmit(true, false, false, false, deterministicAddress); + emit CreatorAttribution(structHash, ZoraCreator1155Attribution.NAME, ZoraCreator1155Attribution.VERSION, creator, signature); + + // create contract and token via premint + preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); + } + /// @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) { From fdaa89dc529196d9647ae1f17febdc62cb15f8fb Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 12 Sep 2023 16:12:43 -0700 Subject: [PATCH 15/27] fix coverage compilation issue --- test/premint/ZoraCreator1155Preminter.t.sol | 36 ++++----------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol index 6d6868b06..9f4a4b60e 100644 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ b/test/premint/ZoraCreator1155Preminter.t.sol @@ -485,43 +485,19 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { ); address firstContractAddress = preminter.getContractAddress(firstContractConfig); - uint256 firstResultTokenId = _signAndExecutePremint( - firstContractConfig, - premintConfig, - creatorPrivateKey, - chainId, - premintExecutor, - quantityToMint, - comment - ); + uint256 tokenId = _signAndExecutePremint(firstContractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, firstResultTokenId), quantityToMint); + assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, tokenId), quantityToMint); premintConfig.uid = secondUid; - uint256 secondResultTokenId = _signAndExecutePremint( - firstContractConfig, - premintConfig, - creatorPrivateKey, - chainId, - premintExecutor, - quantityToMint, - comment - ); + tokenId = _signAndExecutePremint(firstContractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, secondResultTokenId), quantityToMint); + assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, tokenId), quantityToMint); address secondContractAddress = preminter.getContractAddress(secondContractConfig); - uint256 thirdResultTokenId = _signAndExecutePremint( - secondContractConfig, - premintConfig, - creatorPrivateKey, - chainId, - premintExecutor, - quantityToMint, - comment - ); + tokenId = _signAndExecutePremint(secondContractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - assertEq(IZoraCreator1155(secondContractAddress).balanceOf(premintExecutor, thirdResultTokenId), quantityToMint); + assertEq(IZoraCreator1155(secondContractAddress).balanceOf(premintExecutor, tokenId), quantityToMint); } function test_premintCanOnlyBeExecutedAfterStartDate(uint8 startDate, uint8 currentTime) external { From 42b9357d4d50cfe34dc03bb5315efdbc2a6b1497 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 14 Sep 2023 09:48:02 -0700 Subject: [PATCH 16/27] Premint: first minter rewards (#162) * refactor: remove legacy mint fee contracts * refactor: remove unusued import * chore: update tests * chore: lint * chore: update runs * chore: update storage layout * style: update natspec * chore: update tests * chore: remove unused var * chore: update tests * fix: move hardcoded fork vars to constants * added first minter changeset * * Added changeset that depreates redeem minters. * Remove Redeem minter from coverage. --------- Co-authored-by: Rohan Kulkarni --- .changeset/happy-socks-melt.md | 2 +- .../ZoraCreatorRedeemMinterFactory.t.sol | 66 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/.changeset/happy-socks-melt.md b/.changeset/happy-socks-melt.md index e13532db9..35c7e0a04 100644 --- a/.changeset/happy-socks-melt.md +++ b/.changeset/happy-socks-melt.md @@ -2,4 +2,4 @@ "@zoralabs/zora-1155-contracts": minor --- -Adds first minter rewards to zora 1155 contracts. \ No newline at end of file +Adds first minter rewards to zora 1155 contracts. diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol index 738feab88..4992a726e 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol @@ -25,19 +25,19 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test { address payable internal factoryAdmin = payable(address(0x888)); address internal zora; - event RedeemMinterDeployed(address indexed creatorContract, address indexed minterContract); +// event RedeemMinterDeployed(address indexed creatorContract, address indexed minterContract); - function setUp() public { - zora = makeAddr("zora"); - bytes[] memory emptyData = new bytes[](0); - protocolRewards = new ProtocolRewards(); - ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); - Zora1155 proxy = new Zora1155(address(targetImpl)); - target = ZoraCreator1155Impl(address(proxy)); - target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), tokenAdmin, emptyData); +// function setUp() public { +// zora = makeAddr("zora"); +// bytes[] memory emptyData = new bytes[](0); +// protocolRewards = new ProtocolRewards(); +// ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); +// Zora1155 proxy = new Zora1155(address(targetImpl)); +// target = ZoraCreator1155Impl(address(proxy)); +// target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), tokenAdmin, emptyData); - minterFactory = new ZoraCreatorRedeemMinterFactory(); - } +// minterFactory = new ZoraCreatorRedeemMinterFactory(); +// } function test_contractVersion() public { assertEq(minterFactory.contractVersion(), "1.1.0"); @@ -51,30 +51,32 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test { emit RedeemMinterDeployed(address(target), predictedAddress); target.callSale(0, minterFactory, abi.encodeWithSelector(ZoraCreatorRedeemMinterFactory.createMinterIfNoneExists.selector, 0)); vm.stopPrank(); - - ZoraCreatorRedeemMinterStrategy minter = ZoraCreatorRedeemMinterStrategy(predictedAddress); - assertTrue(address(minter).code.length > 0); } +} - function test_createMinterRequiresIZoraCreator1155Caller() public { - ERC1155PresetMinterPauser randomToken = new ERC1155PresetMinterPauser("https://uri.com"); +// ZoraCreatorRedeemMinterStrategy minter = ZoraCreatorRedeemMinterStrategy(predictedAddress); +// assertTrue(address(minter).code.length > 0); +// } - vm.expectRevert(abi.encodeWithSignature("CallerNotZoraCreator1155()")); - vm.prank(address(randomToken)); - minterFactory.createMinterIfNoneExists(); - } +// function test_createMinterRequiresIZoraCreator1155Caller() public { +// ERC1155PresetMinterPauser randomToken = new ERC1155PresetMinterPauser("https://uri.com"); - function test_getDeployedMinterForCreatorContract() public { - vm.prank(address(target)); - minterFactory.createMinterIfNoneExists(); - address minterAddress = minterFactory.predictMinterAddress(address(target)); +// vm.expectRevert(abi.encodeWithSignature("CallerNotZoraCreator1155()")); +// vm.prank(address(randomToken)); +// minterFactory.createMinterIfNoneExists(); +// } - assertEq(minterAddress, minterFactory.getDeployedRedeemMinterForCreatorContract(address(target))); - } +// function test_getDeployedMinterForCreatorContract() public { +// vm.prank(address(target)); +// minterFactory.createMinterIfNoneExists(); +// address minterAddress = minterFactory.predictMinterAddress(address(target)); - function test_supportsInterface() public { - assertTrue(minterFactory.supportsInterface(0x01ffc9a7)); // ERC165 - assertTrue(minterFactory.supportsInterface(type(IMinter1155).interfaceId)); - assertTrue(!minterFactory.supportsInterface(0x6467a6fc)); // old IMinter1155 - } -} +// assertEq(minterAddress, minterFactory.getDeployedRedeemMinterForCreatorContract(address(target))); +// } + +// function test_supportsInterface() public { +// assertTrue(minterFactory.supportsInterface(0x01ffc9a7)); // ERC165 +// assertTrue(minterFactory.supportsInterface(type(IMinter1155).interfaceId)); +// assertTrue(!minterFactory.supportsInterface(0x6467a6fc)); // old IMinter1155 +// } +// } From 82ee3ca7c26a2cf660ac46be40efad9df2269deb Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 14 Sep 2023 10:09:25 -0700 Subject: [PATCH 17/27] Made premint executor upgradeable via a proxy. (#168) Moved some common factory setup logic to some library fixtures --- src/premint/ZoraCreator1155Attribution.sol | 1 + .../ZoraCreator1155PremintExecutor.sol | 35 +- test/premint/ZoraCreator1155Preminter.t.sol | 705 ------------------ 3 files changed, 33 insertions(+), 708 deletions(-) delete mode 100644 test/premint/ZoraCreator1155Preminter.t.sol diff --git a/src/premint/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol index cc3d4bcf7..4c7f5cc2e 100644 --- a/src/premint/ZoraCreator1155Attribution.sol +++ b/src/premint/ZoraCreator1155Attribution.sol @@ -176,6 +176,7 @@ library ZoraCreator1155Attribution { } } +// todo: make it consistent. library PremintTokenSetup { uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index c310eae51..cff98d7d2 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -2,8 +2,9 @@ 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 {UUPSUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "../utils/ownable/Ownable2StepUpgradeable.sol"; +import {IHasContractName} from "../interfaces/IContractMetadata.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol"; import {SharedBaseConstants} from "../shared/SharedBaseConstants.sol"; @@ -15,7 +16,7 @@ import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1 /// 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 { +contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName { IZoraCreator1155Factory public immutable zora1155Factory; /// @notice copied from SharedBaseConstants @@ -30,6 +31,11 @@ contract ZoraCreator1155PremintExecutor { zora1155Factory = _factory; } + function initialize(address _initialOwner) public initializer { + __Ownable_init(_initialOwner); + __UUPSUpgradeable_init(); + } + event Preminted( address indexed contractAddress, uint256 indexed tokenId, @@ -162,4 +168,27 @@ contract ZoraCreator1155PremintExecutor { isValid = IZoraCreator1155(contractAddress).isAdminOrRole(recoveredSigner, CONTRACT_BASE_ID, PERMISSION_BIT_MINTER); } } + + // upgrade related functionality + + /// @notice The name of the contract for upgrade purposes + function contractName() external pure returns (string memory) { + return "ZORA 1155 Premint Executor"; + } + + // upgrade functionality + error UpgradeToMismatchedContractName(string expected, string actual); + + /// @notice Ensures the caller is authorized to upgrade the contract + /// @dev This function is called in `upgradeTo` & `upgradeToAndCall` + /// @param _newImpl The new implementation address + function _authorizeUpgrade(address _newImpl) internal override onlyOwner { + if (!_equals(IHasContractName(_newImpl).contractName(), this.contractName())) { + revert UpgradeToMismatchedContractName(this.contractName(), IHasContractName(_newImpl).contractName()); + } + } + + function _equals(string memory a, string memory b) internal pure returns (bool) { + return (keccak256(bytes(a)) == keccak256(bytes(b))); + } } diff --git a/test/premint/ZoraCreator1155Preminter.t.sol b/test/premint/ZoraCreator1155Preminter.t.sol deleted file mode 100644 index 9f4a4b60e..000000000 --- a/test/premint/ZoraCreator1155Preminter.t.sol +++ /dev/null @@ -1,705 +0,0 @@ -// 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 {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"; -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; - - 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, - uint256 indexed tokenId, - bool indexed createdNewContract, - uint32 uid, - ContractCreationConfig contractConfig, - TokenCreationConfig tokenConfig, - address minter, - uint256 quantityMinted - ); - - 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(mintFeeAmount, zora, address(factoryProxy), address(rewards)); - ZoraCreatorFixedPriceSaleStrategy fixedPriceMinter = new ZoraCreatorFixedPriceSaleStrategy(); - factoryImpl = new ZoraCreator1155FactoryImpl(zoraCreator1155Impl, IMinter1155(address(1)), fixedPriceMinter, IMinter1155(address(3))); - factory = ZoraCreator1155FactoryImpl(address(factoryProxy)); - - vm.startPrank(zora); - factory.upgradeTo(address(factoryImpl)); - factory.initialize(zora); - vm.stopPrank(); - - preminter = new ZoraCreator1155PremintExecutor(factory); - } - - function makeDefaultContractCreationConfig() internal view returns (ContractCreationConfig memory) { - return ContractCreationConfig({contractAdmin: creator, contractName: "blah", contractURI: "blah.contract"}); - } - - function makeDefaultTokenCreationConfig() internal view returns (TokenCreationConfig memory) { - IMinter1155 fixedPriceMinter = factoryImpl.defaultMinters()[0]; - return - TokenCreationConfig({ - tokenURI: "blah.token", - maxSupply: 10, - maxTokensPerAddress: 5, - pricePerToken: 0, - mintStart: 0, - mintDuration: 0, - royaltyMintSchedule: defaultRoyaltyConfig.royaltyMintSchedule, - royaltyBPS: defaultRoyaltyConfig.royaltyBPS, - royaltyRecipient: defaultRoyaltyConfig.royaltyRecipient, - fixedPriceMinter: address(fixedPriceMinter) - }); - } - - 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 - 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 = 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 mintCost = mintFeeAmount * quantityToMint; - // this account will be used to execute the premint, and should result in a contract being created - 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); - - // 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 = 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); - - // 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); - } - - event CreatorAttribution(bytes32 structHash, string domainName, string version, address creator, bytes signature); - - function test_premint_emitsCreatorAttribution_fromErc1155Contract() external { - // build a premint - ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); - - // sign and execute premint - uint256 chainId = block.chainid; - - address deterministicAddress = preminter.getContractAddress(contractConfig); - bytes32 structHash = ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, deterministicAddress, chainId); - bytes memory signature = _sign(creatorPrivateKey, structHash); - - uint256 quantityToMint = 4; - string memory comment = "hi"; - uint256 mintCost = mintFeeAmount * quantityToMint; - // this account will be used to execute the premint, and should result in a contract being created - vm.deal(collector, mintCost); - - vm.prank(collector); - - // verify CreatorAttribution was emitted from the erc1155 contract - vm.expectEmit(true, false, false, false, deterministicAddress); - emit CreatorAttribution(structHash, ZoraCreator1155Attribution.NAME, ZoraCreator1155Attribution.VERSION, creator, signature); - - // create contract and token via premint - preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - } - - /// @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 - 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"); - - // 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); - - // 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 - 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_shouldMintExistingToken() external { - // 1. Make contract creation params - - // configuration of contract to create - ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); - - // how many tokens are minted to the executor - uint256 quantityToMint = 2; - uint256 chainId = block.chainid; - string memory comment = "I love it"; - - address contractAddress = preminter.getContractAddress(contractConfig); - 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 mint tokens for the uid's original token - premintConfig.tokenConfig.tokenURI = "blah2.token"; - bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - - uint256 mintCost = mintFeeAmount * quantityToMint; - vm.deal(collector, mintCost); - - uint256 nextTokenId; - - vm.startPrank(collector); - // premint with new token config and signature, but same uid - it should mint tokens for the first token - nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - - assertEq(nextTokenId, firstTokenId); - assertEq(created1155Contract.balanceOf(collector, firstTokenId), quantityToMint); - - // change the version, it should still point to the first token - premintConfig.version++; - signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - - vm.deal(collector, mintCost); - - // premint with new token config and signature - it should mint tokens for the first token - nextTokenId = preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - vm.stopPrank(); - - 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++; - bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - - 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 { - ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); - - premintConfig.deleted = true; - uint chainId = block.chainid; - 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. - bytes memory signature = _signPremint(contractAddress, premintConfig, creatorPrivateKey, chainId); - - // 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(contractAddress.code.length, 0, "contract has been deployed"); - } - - function test_emitsPremint_whenNewContract() external { - 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(contractAddress, premintConfig, creatorPrivateKey, chainId); - - uint256 expectedTokenId = 1; - - 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; - vm.expectEmit(true, true, true, true); - emit Preminted( - contractAddress, - expectedTokenId, - createdNewContract, - premintConfig.uid, - contractConfig, - premintConfig.tokenConfig, - premintExecutor, - quantityToMint - ); - preminter.premint{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - } - - function test_onlyOwner_hasAdminRights_onCreatedToken() public { - 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 = "I love it"; - - 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); - - ZoraCreatorFixedPriceSaleStrategy.SalesConfig memory newSalesConfig = ZoraCreatorFixedPriceSaleStrategy.SalesConfig({ - pricePerToken: 5 ether, - saleStart: 0, - saleEnd: 0, - maxTokensPerAddress: 5, - fundsRecipient: creator - }); - - IMinter1155 fixedPrice = factoryImpl.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 { - PremintConfig memory premintConfig = makeDefaultPremintConfig(); - - // how many tokens are minted to the executor - uint256 quantityToMint = 4; - uint256 chainId = block.chainid; - string memory comment = "I love it"; - - uint32 firstUid = premintConfig.uid; - uint32 secondUid = firstUid + 1; - - ContractCreationConfig memory firstContractConfig = makeDefaultContractCreationConfig(); - ContractCreationConfig memory secondContractConfig = ContractCreationConfig( - firstContractConfig.contractAdmin, - firstContractConfig.contractURI, - string.concat(firstContractConfig.contractName, "4") - ); - - address firstContractAddress = preminter.getContractAddress(firstContractConfig); - uint256 tokenId = _signAndExecutePremint(firstContractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - - assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, tokenId), quantityToMint); - - premintConfig.uid = secondUid; - tokenId = _signAndExecutePremint(firstContractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - - assertEq(IZoraCreator1155(firstContractAddress).balanceOf(premintExecutor, tokenId), quantityToMint); - - address secondContractAddress = preminter.getContractAddress(secondContractConfig); - tokenId = _signAndExecutePremint(secondContractConfig, premintConfig, creatorPrivateKey, chainId, premintExecutor, quantityToMint, comment); - - assertEq(IZoraCreator1155(secondContractAddress).balanceOf(premintExecutor, tokenId), quantityToMint); - } - - 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); - - ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); - premintConfig.tokenConfig.mintStart = startDate; - - uint256 quantityToMint = 4; - uint256 chainId = block.chainid; - string memory comment = "I love it"; - - // get signature for the premint: - bytes memory signature = _signPremint(preminter.getContractAddress(contractConfig), premintConfig, creatorPrivateKey, chainId); - - if (shouldRevert) { - vm.expectRevert(ZoraCreator1155Attribution.MintNotYetStarted.selector); - } - - uint256 mintCost = mintFeeAmount * quantityToMint; - vm.deal(premintExecutor, mintCost); - - vm.prank(premintExecutor); - preminter.premint{value: mintCost}(contractConfig, 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 - 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(contractAddress, premintConfig, creatorPrivateKey, chainId); - - uint256 quantityToMint = 2; - string memory comment = "I love it"; - - uint256 mintCost = mintFeeAmount * quantityToMint; - vm.deal(premintExecutor, mintCost); - - vm.startPrank(premintExecutor); - - vm.warp(timeOfFirstMint); - 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 = factoryImpl.defaultMinters()[0]; - if (shouldRevert) { - vm.expectRevert(ZoraCreatorFixedPriceSaleStrategy.SaleEnded.selector); - } - - vm.deal(premintExecutor, mintCost); - IZoraCreator1155(contractAddress).mint{value: mintCost}(fixedPriceMinter, tokenId, quantityToMint, abi.encode(premintExecutor, comment)); - - vm.stopPrank(); - } - - 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); - } - - function test_premint_whenContractCreated_premintCanOnlyBeExecutedByPermissionBitMinter() external { - // build a premint - ContractCreationConfig memory contractConfig = makeDefaultContractCreationConfig(); - PremintConfig memory premintConfig = makeDefaultPremintConfig(); - - // 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, premintExecutor, 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); - - 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(premintExecutor); - preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "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); - - vm.deal(premintExecutor, mintCost); - - // try to mint again, should not revert - vm.prank(premintExecutor); - preminter.premint{value: mintCost}(contractConfig, premintConfig2, newCreatorSignature, quantityToMint, "yo"); - } - - function _signAndExecutePremint( - ContractCreationConfig memory contractConfig, - PremintConfig memory premintConfig, - uint256 privateKey, - uint256 chainId, - address executor, - uint256 quantityToMint, - string memory comment - ) 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{value: mintCost}(contractConfig, premintConfig, signature, quantityToMint, comment); - } - - function _signPremint( - address contractAddress, - PremintConfig memory premintConfig, - uint256 privateKey, - uint256 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 - 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); - } -} From d347837a48fe34ef7042206a41a7a185dfa331ae Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 14 Sep 2023 10:13:10 -0700 Subject: [PATCH 18/27] Extracted erc1155 errors to an interface, and importing that into premint contract (#170) --- src/premint/ZoraCreator1155PremintExecutor.sol | 3 ++- test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol | 2 +- test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index cff98d7d2..710ec62bc 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -6,6 +6,7 @@ import {UUPSUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/cont import {Ownable2StepUpgradeable} from "../utils/ownable/Ownable2StepUpgradeable.sol"; import {IHasContractName} from "../interfaces/IContractMetadata.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; +import {IZoraCreator1155Errors} from "../interfaces/IZoraCreator1155Errors.sol"; import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol"; import {SharedBaseConstants} from "../shared/SharedBaseConstants.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; @@ -16,7 +17,7 @@ import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1 /// 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 is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName { +contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName, IZoraCreator1155Errors { IZoraCreator1155Factory public immutable zora1155Factory; /// @notice copied from SharedBaseConstants diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol index 4992a726e..37d9ccfb1 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol @@ -9,7 +9,7 @@ import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.so import {ZoraCreator1155Impl} from "../../../src/nft/ZoraCreator1155Impl.sol"; import {Zora1155} from "../../../src/proxies/Zora1155.sol"; import {IMinter1155} from "../../../src/interfaces/IMinter1155.sol"; -import {IZoraCreator1155} from "../../../src/interfaces/IZoraCreator1155.sol"; +import {IZoraCreator1155Errors} from "../../../src/interfaces/IZoraCreator1155Errors.sol"; import {IRenderer1155} from "../../../src/interfaces/IRenderer1155.sol"; import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol index e2b95c56f..315b12455 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol @@ -8,7 +8,7 @@ import {ERC1155PresetMinterPauser} from "@openzeppelin/contracts/token/ERC1155/p import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; import {ZoraCreator1155Impl} from "../../../src/nft/ZoraCreator1155Impl.sol"; import {Zora1155} from "../../../src/proxies/Zora1155.sol"; -import {IZoraCreator1155} from "../../../src/interfaces/IZoraCreator1155.sol"; +import {IZoraCreator1155Errors} from "../../../src/interfaces/IZoraCreator1155Errors.sol"; import {IRenderer1155} from "../../../src/interfaces/IRenderer1155.sol"; import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; From 91dd71252b3b5e2e2ec45bd484f8ca9495d5c3cc Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 14 Sep 2023 14:31:14 -0700 Subject: [PATCH 19/27] Reverted manual premint version update, and just pointing to origin version. Will do updates via the standard changeset way --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 58fa44067..a04e64b0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zoralabs/zora-1155-contracts", - "version": "1.4.5-gasless", + "version": "1.4.0", "repository": "git@github.com:ourzora/creator-contracts.git", "author": "Iain ", "license": "MIT", From 6322adf9d91a92875d28665c6562c5952b581932 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 22 Sep 2023 11:54:50 -0400 Subject: [PATCH 20/27] updated readme --- uml/gasslessCreate-collecting-activity.puml | 20 ------ uml/gasslessCreate-collecting-sequence.puml | 41 +++++------ uml/gasslessCreate-creation-activity.puml | 27 ------- uml/gasslessCreate-creation-sequence.puml | 71 +++++++++++-------- .../gasslessCreate-collecting-activity.svg | 1 - 5 files changed, 56 insertions(+), 104 deletions(-) delete mode 100644 uml/gasslessCreate-collecting-activity.puml delete mode 100644 uml/gasslessCreate-creation-activity.puml delete mode 100644 uml/generated/gasslessCreate-collecting-activity.svg diff --git a/uml/gasslessCreate-collecting-activity.puml b/uml/gasslessCreate-collecting-activity.puml deleted file mode 100644 index c0d41f3b5..000000000 --- a/uml/gasslessCreate-collecting-activity.puml +++ /dev/null @@ -1,20 +0,0 @@ -@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 index d6c017fdb..869ae1fea 100644 --- a/uml/gasslessCreate-collecting-sequence.puml +++ b/uml/gasslessCreate-collecting-sequence.puml @@ -1,38 +1,29 @@ @startuml actor Collector -entity PremintCollectPage +entity CollectUI +entity Wallet boundary SignatureAPI -entity SignatureDB -entity PreminterContract +entity PremintExecutorContract entity 1155FactoryContract entity 1155Contract -Collector -> PremintCollectPage: Open, param is \ndeterministic collection address\n+ token uid -Activate PremintCollectPage -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 -PremintCollectPage -> PreminterContract: Check if signature has been used (by contract hash + token uid) -PreminterContract --> PremintCollectPage: Signature has been used or not +Collector -> CollectUI: Open, param is \ndeterministic collection address\n+ token uid +Activate CollectUI +CollectUI -> SignatureAPI: Fetch by:\ncollection address, premint uid +SignatureAPI --> CollectUI: contract creation params,\ntoken creation params,\nsignature -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 +Collector -> CollectUI: mint +CollectUI -> Wallet: Submit premint transaction +deactivate CollectUI +Wallet -> PremintExecutorContract: premint(collectionConfig, tokenConfig, uid, signature) +activate PremintExecutorContract Group contract doesnt exist - PreminterContract -> 1155FactoryContract: create contract + PremintExecutorContract -> 1155FactoryContract: create contract + activate 1155FactoryContract 1155FactoryContract -> 1155Contract: create + deactivate 1155FactoryContract activate 1155Contract end @@ -41,7 +32,7 @@ PreminterContract -> 1155Contract: create new token\nwith signature PreminterContract -> 1155Contract: set new token sale parameters PreminterContract -> 1155Contract: mint tokens to collector -deactivate PreminterContract +deactivate PremintExecutorContract 1155Contract --> Collector: Minted tokens deactivate 1155Contract diff --git a/uml/gasslessCreate-creation-activity.puml b/uml/gasslessCreate-creation-activity.puml deleted file mode 100644 index 363927823..000000000 --- a/uml/gasslessCreate-creation-activity.puml +++ /dev/null @@ -1,27 +0,0 @@ -@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 collection address; - endif - :Get new uid\nfrom backend server; -else (no) - :load existing\ncontract + token creation parameters\nby collection address + uid; -endif -:Ask creator for new\ntoken creation params; -:Request signature with:\ncollection address + token params + uid; -:Submit to backend server:\ncollection + token params + uid + signature\n; - -stop - -@enduml diff --git a/uml/gasslessCreate-creation-sequence.puml b/uml/gasslessCreate-creation-sequence.puml index 1d7213f43..37ea85acc 100644 --- a/uml/gasslessCreate-creation-sequence.puml +++ b/uml/gasslessCreate-creation-sequence.puml @@ -1,51 +1,60 @@ @startuml -title Creating a signature for a new erc1155 contract + token +title Creating a signature for a Premint Erc1155 contract + New token actor Creator -entity CreatePage -boundary SignatureAPI +entity Wallet +entity CreateUI +boundary PremintAPI boundary PremintContract -entity SignatureDB - -Group Signature not created for contract yet +Group New premint token on new contract - 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 + Creator -> CreateUI: setup NEW contract name + image + activate CreateUI + CreateUI -> PremintContract: get determnistic collection address\nfor contract creation params + activate PremintContract + PremintContract --> CreateUI: determinstic collection address + deactivate CreateUI + deactivate PremintContract end -Group Signature has been created for contract +Group New premint token on existing premint contract + + Creator -> CreateUI: load page to create new token for\npremint at determinstic\ncollection address + activate CreateUI + CreateUI -> PremintAPI: load collection creation params\nby determinstic address + activate PremintAPI - 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 + Group Premint exists + PremintAPI --> CreateUI: collection creation params\n(from premint) + deactivate CreateUI + deactivate PremintAPI + end end -Creator -> CreatePage: setup new token -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 -> PremintContract: validate signature -PremintContract --> SignatureAPI: validation results (true/false & recovered signer) +CreateUI -> PremintAPI: get next uid for\ncollection address +activate CreateUI +activate PremintAPI +PremintAPI --> CreateUI: next uid for collection -Group Signature is valid +Creator -> CreateUI: configure new token parameters +Creator -> CreateUI: Submit +CreateUI -> Creator: request Premint EIP-712 signature containing:\n token creation params, token uid, version +Creator -> Wallet: sign message +Wallet -> CreateUI: Signed message by creator of\ntoken creation params, token uid, version +CreateUI -> PremintAPI: validate and store signature +PremintAPI -> PremintContract: validate signature +PremintContract --> PremintAPI: validation results (true/false & recovered signer) - SignatureAPI -> SignatureDB: store signature + \ncontract creation + \ntoken creation params + \ncollection address + \ntoken uid +Group Signature is valid + PremintAPI -> PremintAPI: store premint and signature end +PremintAPI -> CreateUI: validation & storage status +deactivate CreateUI + @enduml \ No newline at end of file diff --git a/uml/generated/gasslessCreate-collecting-activity.svg b/uml/generated/gasslessCreate-collecting-activity.svg deleted file mode 100644 index 141d18db0..000000000 --- a/uml/generated/gasslessCreate-collecting-activity.svg +++ /dev/null @@ -1 +0,0 @@ -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 From 851d702e05046de899dbded7a9a7d999356cade9 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 22 Sep 2023 12:59:53 -0400 Subject: [PATCH 21/27] updating natspec --- src/premint/ZoraCreator1155PremintExecutor.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index 710ec62bc..5abd63758 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -138,6 +138,10 @@ contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradea /// @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. + /// @param contractAddress address of contract to get status for + /// @param uid of premint to check status for + /// @return contractCreated if the 1155 contract has been created + /// @return tokenIdForPremint If the token has been created for the premint, the token id, otherwise 0 function premintStatus(address contractAddress, uint32 uid) public view returns (bool contractCreated, uint256 tokenIdForPremint) { if (contractAddress.code.length == 0) { return (false, 0); From 96304aedcd2784b9eeb7249975db23e54d91a104 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Fri, 22 Sep 2023 13:22:53 -0400 Subject: [PATCH 22/27] updated natspec readme --- src/nft/ZoraCreator1155Impl.sol | 4 +++ .../ZoraCreator1155PremintExecutor.sol | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/nft/ZoraCreator1155Impl.sol b/src/nft/ZoraCreator1155Impl.sol index be267bc3b..d32e69f59 100644 --- a/src/nft/ZoraCreator1155Impl.sol +++ b/src/nft/ZoraCreator1155Impl.sol @@ -795,6 +795,10 @@ contract ZoraCreator1155Impl is return _getImplementation(); } + /// Sets up a new token using a token configuration and a signature created for the token creation parameters. + /// The signature must be created by an account with the PERMISSION_BIT_MINTER role on the contract. + /// @param premintConfig configuration of token to be created + /// @param signature EIP-712 Signature created on the premintConfig by an account with the PERMISSION_BIT_MINTER role on the contract. 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) { diff --git a/src/premint/ZoraCreator1155PremintExecutor.sol b/src/premint/ZoraCreator1155PremintExecutor.sol index 5abd63758..2d4b93dc6 100644 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ b/src/premint/ZoraCreator1155PremintExecutor.sol @@ -16,6 +16,7 @@ import {PremintConfig, ContractCreationConfig, TokenCreationConfig, ZoraCreator1 /// @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. +/// Stateless contract that just executes atomic transactions for delegated contract and token creation and minting. /// @author @oveddan contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName, IZoraCreator1155Errors { IZoraCreator1155Factory public immutable zora1155Factory; @@ -32,11 +33,22 @@ contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradea zora1155Factory = _factory; } + /// Initialized the upgradeable contract + /// @param _initialOwner The owner of the contract function initialize(address _initialOwner) public initializer { __Ownable_init(_initialOwner); __UUPSUpgradeable_init(); } + /// @notice Emitted when a new token is created and minted to the executor of the transaction + /// @param contractAddress address of erc1155 contract that premint was executed against. This contract would have been created by the premint call if it didn't already exist. + /// @param tokenId Created token id as a result of the premint action + /// @param createdNewContract If a new contract was created as a result of the premint action + /// @param uid uid of the signed premint + /// @param contractConfig Configuration of contract that was created, if one was created + /// @param tokenConfig Configuration of token that was created + /// @param minter Address of the minter that executed the premint action + /// @param quantityMinted Quantity of tokens that were minted to the executor of the transaction event Preminted( address indexed contractAddress, uint256 indexed tokenId, @@ -48,14 +60,14 @@ contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradea 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. + /// @notice 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. + /// Contract address of the created contract is deterministically generated from the contract config, this contract's address, and the erc1155 factory contract's address. + /// @param contractConfig Parameters for creating a new erc1155 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 signature Signature of the creator of the premint. /// @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( @@ -123,9 +135,10 @@ contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradea 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. + /// @notice Gets the deterministic contract address for the given contract creation config. + /// Contract address is generated deterministically from this premint contract's address, the factory's address, and a hash based on the contract uri, contract name, + /// contract admin. + /// @param contractConfig Configuration of the contract to get the address for function getContractAddress(ContractCreationConfig calldata contractConfig) public view returns (address) { return zora1155Factory.deterministicContractAddress(address(this), contractConfig.contractURI, contractConfig.contractName, contractConfig.contractAdmin); @@ -174,7 +187,7 @@ contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradea } } - // upgrade related functionality + // begin upgrade related functionality /// @notice The name of the contract for upgrade purposes function contractName() external pure returns (string memory) { From 22fe7ac64d58e1eeddb92c48217fb55febd234a5 Mon Sep 17 00:00:00 2001 From: iain nash Date: Wed, 27 Sep 2023 17:02:34 -0400 Subject: [PATCH 23/27] remove unnecessary changes --- .env.anvil | 3 +- script/DeployPreminter.s.sol | 80 ------ src/premint/README.md | 59 ----- src/premint/ZoraCreator1155Attribution.sol | 240 ------------------ .../ZoraCreator1155PremintExecutor.sol | 212 ---------------- .../ZoraCreatorRedeemMinterFactory.t.sol | 68 +++-- .../ZoraCreatorRedeemMinterStrategy.t.sol | 2 +- 7 files changed, 35 insertions(+), 629 deletions(-) delete mode 100644 script/DeployPreminter.s.sol delete mode 100644 src/premint/README.md delete mode 100644 src/premint/ZoraCreator1155Attribution.sol delete mode 100644 src/premint/ZoraCreator1155PremintExecutor.sol diff --git a/.env.anvil b/.env.anvil index 4e183d130..0c2078aa6 100644 --- a/.env.anvil +++ b/.env.anvil @@ -1,3 +1,2 @@ FORK_RPC_URL="https://testnet.rpc.zora.co/" -# FORK_BLOCK_NUMBER=916572 -FORK_BLOCK_NUMBER=700700 +FORK_BLOCK_NUMBER=916572 \ No newline at end of file diff --git a/script/DeployPreminter.s.sol b/script/DeployPreminter.s.sol deleted file mode 100644 index 4126fd9b1..000000000 --- a/script/DeployPreminter.s.sol +++ /dev/null @@ -1,80 +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 {ZoraCreator1155PremintExecutor} from "../src/premint/ZoraCreator1155PremintExecutor.sol"; - -contract DeployPreminter is ZoraDeployerBase { - function run() public returns (string memory) { - Deployment memory deployment = getDeployment(); - - uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - - // bool deployFactory = vm.envBool("DEPLOY_FACTORY"); - bool deployFactory = vm.envBool("DEPLOY_FACTORY"); - - IZoraCreator1155Factory factoryProxy; - vm.startBroadcast(deployerPrivateKey); - - 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(); - - deployment.preminter = address(preminter); - - return getDeploymentJSON(deployment); - } -} diff --git a/src/premint/README.md b/src/premint/README.md deleted file mode 100644 index 6b65baff7..000000000 --- a/src/premint/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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/ZoraCreator1155Attribution.sol b/src/premint/ZoraCreator1155Attribution.sol deleted file mode 100644 index 4c7f5cc2e..000000000 --- a/src/premint/ZoraCreator1155Attribution.sol +++ /dev/null @@ -1,240 +0,0 @@ -// 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 */ - string internal constant NAME = "Preminter"; - string internal constant VERSION = "1"; - bytes32 internal constant HASHED_NAME = keccak256(bytes(NAME)); - bytes32 internal constant HASHED_VERSION = keccak256(bytes(VERSION)); - bytes32 internal 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); - } -} - -// todo: make it consistent. -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 deleted file mode 100644 index 2d4b93dc6..000000000 --- a/src/premint/ZoraCreator1155PremintExecutor.sol +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; -import {UUPSUpgradeable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "../utils/ownable/Ownable2StepUpgradeable.sol"; -import {IHasContractName} from "../interfaces/IContractMetadata.sol"; -import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; -import {IZoraCreator1155Errors} from "../interfaces/IZoraCreator1155Errors.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. -/// Stateless contract that just executes atomic transactions for delegated contract and token creation and minting. -/// @author @oveddan -contract ZoraCreator1155PremintExecutor is Ownable2StepUpgradeable, UUPSUpgradeable, IHasContractName, IZoraCreator1155Errors { - IZoraCreator1155Factory public immutable zora1155Factory; - - /// @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) { - zora1155Factory = _factory; - } - - /// Initialized the upgradeable contract - /// @param _initialOwner The owner of the contract - function initialize(address _initialOwner) public initializer { - __Ownable_init(_initialOwner); - __UUPSUpgradeable_init(); - } - - /// @notice Emitted when a new token is created and minted to the executor of the transaction - /// @param contractAddress address of erc1155 contract that premint was executed against. This contract would have been created by the premint call if it didn't already exist. - /// @param tokenId Created token id as a result of the premint action - /// @param createdNewContract If a new contract was created as a result of the premint action - /// @param uid uid of the signed premint - /// @param contractConfig Configuration of contract that was created, if one was created - /// @param tokenConfig Configuration of token that was created - /// @param minter Address of the minter that executed the premint action - /// @param quantityMinted Quantity of tokens that were minted to the executor of the transaction - event Preminted( - address indexed contractAddress, - uint256 indexed tokenId, - bool indexed createdNewContract, - uint32 uid, - ContractCreationConfig contractConfig, - TokenCreationConfig tokenConfig, - address minter, - uint256 quantityMinted - ); - - /// @notice 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, this contract's address, and the erc1155 factory contract's address. - /// @param contractConfig Parameters for creating a new erc1155 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 premint. - /// @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); - - // 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); - - tokenContract.mint{value: msg.value}( - IMinter1155(premintConfig.tokenConfig.fixedPriceMinter), - newTokenId, - quantityToMint, - abi.encode(msg.sender, mintComment) - ); - - // emit Preminted event - 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) { - 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 = zora1155Factory.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); - } - - /// @notice Gets the deterministic contract address for the given contract creation config. - /// Contract address is generated deterministically from this premint contract's address, the factory's address, and a hash based on the contract uri, contract name, - /// contract admin. - /// @param contractConfig Configuration of the contract to get the address for - function getContractAddress(ContractCreationConfig calldata contractConfig) public view returns (address) { - 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. - 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. - /// @param contractAddress address of contract to get status for - /// @param uid of premint to check status for - /// @return contractCreated if the 1155 contract has been created - /// @return tokenIdForPremint If the token has been created for the premint, the token id, otherwise 0 - 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); - } - } - - // begin upgrade related functionality - - /// @notice The name of the contract for upgrade purposes - function contractName() external pure returns (string memory) { - return "ZORA 1155 Premint Executor"; - } - - // upgrade functionality - error UpgradeToMismatchedContractName(string expected, string actual); - - /// @notice Ensures the caller is authorized to upgrade the contract - /// @dev This function is called in `upgradeTo` & `upgradeToAndCall` - /// @param _newImpl The new implementation address - function _authorizeUpgrade(address _newImpl) internal override onlyOwner { - if (!_equals(IHasContractName(_newImpl).contractName(), this.contractName())) { - revert UpgradeToMismatchedContractName(this.contractName(), IHasContractName(_newImpl).contractName()); - } - } - - function _equals(string memory a, string memory b) internal pure returns (bool) { - return (keccak256(bytes(a)) == keccak256(bytes(b))); - } -} diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol index 37d9ccfb1..738feab88 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterFactory.t.sol @@ -9,7 +9,7 @@ import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.so import {ZoraCreator1155Impl} from "../../../src/nft/ZoraCreator1155Impl.sol"; import {Zora1155} from "../../../src/proxies/Zora1155.sol"; import {IMinter1155} from "../../../src/interfaces/IMinter1155.sol"; -import {IZoraCreator1155Errors} from "../../../src/interfaces/IZoraCreator1155Errors.sol"; +import {IZoraCreator1155} from "../../../src/interfaces/IZoraCreator1155.sol"; import {IRenderer1155} from "../../../src/interfaces/IRenderer1155.sol"; import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; @@ -25,19 +25,19 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test { address payable internal factoryAdmin = payable(address(0x888)); address internal zora; -// event RedeemMinterDeployed(address indexed creatorContract, address indexed minterContract); + event RedeemMinterDeployed(address indexed creatorContract, address indexed minterContract); -// function setUp() public { -// zora = makeAddr("zora"); -// bytes[] memory emptyData = new bytes[](0); -// protocolRewards = new ProtocolRewards(); -// ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); -// Zora1155 proxy = new Zora1155(address(targetImpl)); -// target = ZoraCreator1155Impl(address(proxy)); -// target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), tokenAdmin, emptyData); + function setUp() public { + zora = makeAddr("zora"); + bytes[] memory emptyData = new bytes[](0); + protocolRewards = new ProtocolRewards(); + ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(0, zora, address(0), address(protocolRewards)); + Zora1155 proxy = new Zora1155(address(targetImpl)); + target = ZoraCreator1155Impl(address(proxy)); + target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), tokenAdmin, emptyData); -// minterFactory = new ZoraCreatorRedeemMinterFactory(); -// } + minterFactory = new ZoraCreatorRedeemMinterFactory(); + } function test_contractVersion() public { assertEq(minterFactory.contractVersion(), "1.1.0"); @@ -51,32 +51,30 @@ contract ZoraCreatorRedeemMinterFactoryTest is Test { emit RedeemMinterDeployed(address(target), predictedAddress); target.callSale(0, minterFactory, abi.encodeWithSelector(ZoraCreatorRedeemMinterFactory.createMinterIfNoneExists.selector, 0)); vm.stopPrank(); - } -} -// ZoraCreatorRedeemMinterStrategy minter = ZoraCreatorRedeemMinterStrategy(predictedAddress); -// assertTrue(address(minter).code.length > 0); -// } + ZoraCreatorRedeemMinterStrategy minter = ZoraCreatorRedeemMinterStrategy(predictedAddress); + assertTrue(address(minter).code.length > 0); + } -// function test_createMinterRequiresIZoraCreator1155Caller() public { -// ERC1155PresetMinterPauser randomToken = new ERC1155PresetMinterPauser("https://uri.com"); + function test_createMinterRequiresIZoraCreator1155Caller() public { + ERC1155PresetMinterPauser randomToken = new ERC1155PresetMinterPauser("https://uri.com"); -// vm.expectRevert(abi.encodeWithSignature("CallerNotZoraCreator1155()")); -// vm.prank(address(randomToken)); -// minterFactory.createMinterIfNoneExists(); -// } + vm.expectRevert(abi.encodeWithSignature("CallerNotZoraCreator1155()")); + vm.prank(address(randomToken)); + minterFactory.createMinterIfNoneExists(); + } -// function test_getDeployedMinterForCreatorContract() public { -// vm.prank(address(target)); -// minterFactory.createMinterIfNoneExists(); -// address minterAddress = minterFactory.predictMinterAddress(address(target)); + function test_getDeployedMinterForCreatorContract() public { + vm.prank(address(target)); + minterFactory.createMinterIfNoneExists(); + address minterAddress = minterFactory.predictMinterAddress(address(target)); -// assertEq(minterAddress, minterFactory.getDeployedRedeemMinterForCreatorContract(address(target))); -// } + assertEq(minterAddress, minterFactory.getDeployedRedeemMinterForCreatorContract(address(target))); + } -// function test_supportsInterface() public { -// assertTrue(minterFactory.supportsInterface(0x01ffc9a7)); // ERC165 -// assertTrue(minterFactory.supportsInterface(type(IMinter1155).interfaceId)); -// assertTrue(!minterFactory.supportsInterface(0x6467a6fc)); // old IMinter1155 -// } -// } + function test_supportsInterface() public { + assertTrue(minterFactory.supportsInterface(0x01ffc9a7)); // ERC165 + assertTrue(minterFactory.supportsInterface(type(IMinter1155).interfaceId)); + assertTrue(!minterFactory.supportsInterface(0x6467a6fc)); // old IMinter1155 + } +} diff --git a/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol b/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol index 315b12455..e2b95c56f 100644 --- a/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol +++ b/test/minters/redeem/ZoraCreatorRedeemMinterStrategy.t.sol @@ -8,7 +8,7 @@ import {ERC1155PresetMinterPauser} from "@openzeppelin/contracts/token/ERC1155/p import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; import {ZoraCreator1155Impl} from "../../../src/nft/ZoraCreator1155Impl.sol"; import {Zora1155} from "../../../src/proxies/Zora1155.sol"; -import {IZoraCreator1155Errors} from "../../../src/interfaces/IZoraCreator1155Errors.sol"; +import {IZoraCreator1155} from "../../../src/interfaces/IZoraCreator1155.sol"; import {IRenderer1155} from "../../../src/interfaces/IRenderer1155.sol"; import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltiesControl.sol"; import {IZoraCreator1155Factory} from "../../../src/interfaces/IZoraCreator1155Factory.sol"; From 34794cf838e5d28ae2013c47bd586a2ec7ee5c17 Mon Sep 17 00:00:00 2001 From: iain nash Date: Wed, 27 Sep 2023 17:05:13 -0400 Subject: [PATCH 24/27] update uml diagrams --- uml/generated/gasslessCreate-collecting-sequence.svg | 2 +- uml/generated/gasslessCreate-creation-sequence.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uml/generated/gasslessCreate-collecting-sequence.svg b/uml/generated/gasslessCreate-collecting-sequence.svg index 8c3d4d2ad..06c41255c 100644 --- a/uml/generated/gasslessCreate-collecting-sequence.svg +++ b/uml/generated/gasslessCreate-collecting-sequence.svg @@ -1 +1 @@ -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 +CollectorCollectorCollectUICollectUIWalletWalletSignatureAPISignatureAPIPremintExecutorContractPremintExecutorContract1155FactoryContract1155FactoryContract1155Contract1155ContractPreminterContractPreminterContractOpen, param isdeterministic collection address+ token uidFetch by:collection address, premint uidcontract creation params,token creation params,signaturemintSubmit premint transactionpremint(collectionConfig, tokenConfig, uid, signature)contract doesnt existcreate contractcreatecreate new tokenwith signatureset new token sale parametersmint tokens to collectorMinted tokens \ No newline at end of file diff --git a/uml/generated/gasslessCreate-creation-sequence.svg b/uml/generated/gasslessCreate-creation-sequence.svg index f5005aaea..920596d75 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 + 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 +Creating a signature for a Premint Erc1155 contract + New tokenCreatorCreatorWalletWalletCreateUICreateUIPremintAPIPremintAPIPremintContractPremintContractNew premint token on new contractsetup NEW contract name + imageget determnistic collection addressfor contract creation paramsdeterminstic collection addressNew premint token on existing premint contractload page to create new token forpremint at determinsticcollection addressload collection creation paramsby determinstic addressPremint existscollection creation params(from premint)get next uid forcollection addressnext uid for collectionconfigure new token parametersSubmitrequest Premint EIP-712 signature containing:token creation params, token uid, versionsign messageSigned message by creator oftoken creation params, token uid, versionvalidate and store signaturevalidate signaturevalidation results (true/false & recovered signer)Signature is validstore premint and signaturevalidation & storage status \ No newline at end of file From 71c264884b66f889ca6671b39fdf6e119ad2b856 Mon Sep 17 00:00:00 2001 From: iain nash Date: Wed, 27 Sep 2023 17:15:13 -0400 Subject: [PATCH 25/27] fix build --- src/interfaces/IZoraCreator1155.sol | 2 -- test/factory/ZoraCreator1155Factory_Fork.t.sol | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/interfaces/IZoraCreator1155.sol b/src/interfaces/IZoraCreator1155.sol index f571b835d..1488def9c 100644 --- a/src/interfaces/IZoraCreator1155.sol +++ b/src/interfaces/IZoraCreator1155.sol @@ -84,8 +84,6 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IZoraCreator1155Errors, I 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/test/factory/ZoraCreator1155Factory_Fork.t.sol b/test/factory/ZoraCreator1155Factory_Fork.t.sol index 82a083834..921af2e29 100644 --- a/test/factory/ZoraCreator1155Factory_Fork.t.sol +++ b/test/factory/ZoraCreator1155Factory_Fork.t.sol @@ -15,11 +15,8 @@ import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/Z import {ForkDeploymentConfig} from "../../src/deployment/DeploymentConfig.sol"; contract ZoraCreator1155FactoryForkTest is ForkDeploymentConfig, Test { - uint96 constant tokenPrice = 1 ether; uint256 constant quantityToMint = 3; uint256 constant tokenMaxSupply = 100; - uint32 constant royaltyMintSchedule = 10; - uint32 constant royaltyBPS = 100; address collector; address creator; @@ -96,6 +93,7 @@ contract ZoraCreator1155FactoryForkTest is ForkDeploymentConfig, Test { } function testTheFork(string memory chainName) private { + uint96 tokenPrice = 1 ether; console.log("testing on fork: ", chainName); // create and select the fork, which will be used for all subsequent calls From 85121f8060bfee65946b18c5b3cf030b737b0840 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 28 Sep 2023 07:59:19 -0400 Subject: [PATCH 26/27] Undo changes to changesets --- .changeset/happy-socks-melt.md | 2 +- .changeset/long-avocados-visit.md | 4 +--- .changeset/twelve-comics-sniff.md | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.changeset/happy-socks-melt.md b/.changeset/happy-socks-melt.md index 35c7e0a04..e13532db9 100644 --- a/.changeset/happy-socks-melt.md +++ b/.changeset/happy-socks-melt.md @@ -2,4 +2,4 @@ "@zoralabs/zora-1155-contracts": minor --- -Adds first minter rewards to zora 1155 contracts. +Adds first minter rewards to zora 1155 contracts. \ No newline at end of file diff --git a/.changeset/long-avocados-visit.md b/.changeset/long-avocados-visit.md index 953539c64..581c4e430 100644 --- a/.changeset/long-avocados-visit.md +++ b/.changeset/long-avocados-visit.md @@ -2,6 +2,4 @@ "@zoralabs/zora-1155-contracts": minor --- -- Added deterministic contract creation from the Zora1155 factory -- Added gasless minter -- Added deterministic contract creation +Added deterministic contract creation from the Zora1155 factory diff --git a/.changeset/twelve-comics-sniff.md b/.changeset/twelve-comics-sniff.md index 871e80dd9..e251c6a53 100644 --- a/.changeset/twelve-comics-sniff.md +++ b/.changeset/twelve-comics-sniff.md @@ -2,4 +2,4 @@ "@zoralabs/zora-1155-contracts": minor --- -Added the PremintExecutor contract, and updated erc1155 to support delegated minting +Added the PremintExecutor contract, and updated erc1155 to support delegated minting \ No newline at end of file From 7b1463691ef0798740a81958a479b0b34107e14f Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Thu, 28 Sep 2023 08:03:30 -0400 Subject: [PATCH 27/27] cleaned up readme, and removed some uml --- src/delegation/README.md | 9 +-------- uml/generated/gasslessCreate-creation-activity.svg | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 uml/generated/gasslessCreate-creation-activity.svg diff --git a/src/delegation/README.md b/src/delegation/README.md index 6b65baff7..d073231a2 100644 --- a/src/delegation/README.md +++ b/src/delegation/README.md @@ -14,8 +14,7 @@ A Preminter contract validates signatures and executes actions to 1. deploy cont `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. + * **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, as that combination is used to determinstically determine the contract address. 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. @@ -41,17 +40,11 @@ Functions: 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 diff --git a/uml/generated/gasslessCreate-creation-activity.svg b/uml/generated/gasslessCreate-creation-activity.svg deleted file mode 100644 index dc92d51b9..000000000 --- a/uml/generated/gasslessCreate-creation-activity.svg +++ /dev/null @@ -1 +0,0 @@ -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