diff --git a/.changeset/clean-eagles-jam.md b/.changeset/clean-eagles-jam.md new file mode 100644 index 000000000..6ff98acd9 --- /dev/null +++ b/.changeset/clean-eagles-jam.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/zora-1155-contracts": patch +--- + +Adds first minter reward to ERC20 Minter diff --git a/.changeset/flat-carpets-drum.md b/.changeset/flat-carpets-drum.md new file mode 100644 index 000000000..969e8f37f --- /dev/null +++ b/.changeset/flat-carpets-drum.md @@ -0,0 +1,5 @@ +--- +"nft-creator-subgraph": minor +--- + +Adds ERC20Minter contract and events to the subgraph diff --git a/.changeset/neat-clocks-serve.md b/.changeset/neat-clocks-serve.md new file mode 100644 index 000000000..4d8c20aaf --- /dev/null +++ b/.changeset/neat-clocks-serve.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/zora-1155-contracts": minor +--- + +Adds ERC20 Minter contract which enables NFTs to be purchased with ERC20 tokens diff --git a/.changeset/yellow-ducks-melt.md b/.changeset/yellow-ducks-melt.md new file mode 100644 index 000000000..f1638f190 --- /dev/null +++ b/.changeset/yellow-ducks-melt.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/nft-creator-subgraph": minor +--- + +Adds ERC20Minter deployment addresses to the subgraph configs diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 136159ade..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "solidity.compileUsingRemoteVersion": "v0.8.17+commit.8df45f5f" -} \ No newline at end of file diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol index d596e1dff..f109673e9 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155Attribution.sol @@ -3,14 +3,15 @@ pragma solidity 0.8.17; import {IMinter1155} from "../interfaces/IMinter1155.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; -import {IZoraCreator1155Errors} from "../interfaces/IZoraCreator1155Errors.sol"; +import {IZoraCreator1155Errors} from "@zoralabs/shared-contracts/interfaces/errors/IZoraCreator1155Errors.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"; import {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; +import {IERC20Minter, ERC20Minter} from "../minters/erc20/ERC20Minter.sol"; import {IERC1271} from "../interfaces/IERC1271.sol"; -import {PremintConfig, ContractCreationConfig, TokenCreationConfig, PremintConfigV2, TokenCreationConfigV2} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {PremintConfig, ContractCreationConfig, TokenCreationConfig, PremintConfigV2, TokenCreationConfigV2, Erc20TokenCreationConfigV1, Erc20PremintConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol"; library ZoraCreator1155Attribution { string internal constant NAME = "Preminter"; @@ -116,6 +117,18 @@ library ZoraCreator1155Attribution { ); } + bytes32 constant ATTRIBUTION_DOMAIN_ERC20_V1 = + keccak256( + "CreatorAttribution(TokenCreationConfig tokenConfig,uint32 uid,uint32 version,bool deleted)TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,address erc20Minter,uint64 mintStart,uint64 mintDuration,uint64 maxTokensPerAddress,address currency,uint256 pricePerToken)" + ); + + function hashPremint(Erc20PremintConfigV1 memory premintConfig) internal pure returns (bytes32) { + return + keccak256( + abi.encode(ATTRIBUTION_DOMAIN_ERC20_V1, _hashToken(premintConfig.tokenConfig), premintConfig.uid, premintConfig.version, premintConfig.deleted) + ); + } + bytes32 constant TOKEN_DOMAIN_V1 = keccak256( "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint64 maxTokensPerAddress,uint96 pricePerToken,uint64 mintStart,uint64 mintDuration,uint32 royaltyMintSchedule,uint32 royaltyBPS,address royaltyRecipient,address fixedPriceMinter)" @@ -164,6 +177,31 @@ library ZoraCreator1155Attribution { ); } + bytes32 constant TOKEN_DOMAIN_ERC20_V1 = + keccak256( + "TokenCreationConfig(string tokenURI,uint256 maxSupply,uint32 royaltyBPS,address payoutRecipient,address createReferral,address erc20Minter,uint64 mintStart,uint64 mintDuration,uint64 maxTokensPerAddress,address currency,uint256 pricePerToken)" + ); + + function _hashToken(Erc20TokenCreationConfigV1 memory tokenConfig) private pure returns (bytes32) { + return + keccak256( + abi.encode( + TOKEN_DOMAIN_ERC20_V1, + _stringHash(tokenConfig.tokenURI), + tokenConfig.maxSupply, + tokenConfig.royaltyBPS, + tokenConfig.payoutRecipient, + tokenConfig.createReferral, + tokenConfig.erc20Minter, + tokenConfig.mintStart, + tokenConfig.mintDuration, + tokenConfig.maxTokensPerAddress, + tokenConfig.currency, + tokenConfig.pricePerToken + ) + ); + } + bytes32 internal constant TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); function _stringHash(string memory value) private pure returns (bytes32) { @@ -195,6 +233,21 @@ library ZoraCreator1155Attribution { library PremintTokenSetup { uint256 constant PERMISSION_BIT_MINTER = 2 ** 2; + /// @notice Build token setup actions for a v3 preminted token + function makeSetupNewTokenCalls(uint256 newTokenId, Erc20TokenCreationConfigV1 memory tokenConfig) internal view returns (bytes[] memory calls) { + return + _buildCalls({ + newTokenId: newTokenId, + erc20MinterAddress: tokenConfig.erc20Minter, + currency: tokenConfig.currency, + pricePerToken: tokenConfig.pricePerToken, + maxTokensPerAddress: tokenConfig.maxTokensPerAddress, + mintDuration: tokenConfig.mintDuration, + royaltyBPS: tokenConfig.royaltyBPS, + payoutRecipient: tokenConfig.payoutRecipient + }); + } + /// @notice Build token setup actions for a v2 preminted token function makeSetupNewTokenCalls(uint256 newTokenId, TokenCreationConfigV2 memory tokenConfig) internal view returns (bytes[] memory calls) { return @@ -223,6 +276,38 @@ library PremintTokenSetup { }); } + function _buildCalls( + uint256 newTokenId, + address erc20MinterAddress, + address currency, + uint256 pricePerToken, + uint64 maxTokensPerAddress, + uint64 mintDuration, + uint32 royaltyBPS, + address payoutRecipient + ) private view returns (bytes[] memory calls) { + calls = new bytes[](3); + + calls[0] = abi.encodeWithSelector(IZoraCreator1155.addPermission.selector, newTokenId, erc20MinterAddress, PERMISSION_BIT_MINTER); + + calls[1] = abi.encodeWithSelector( + IZoraCreator1155.callSale.selector, + newTokenId, + IMinter1155(erc20MinterAddress), + abi.encodeWithSelector( + IERC20Minter.setSale.selector, + newTokenId, + _buildNewERC20SalesConfig(currency, pricePerToken, maxTokensPerAddress, mintDuration, payoutRecipient) + ) + ); + + calls[2] = abi.encodeWithSelector( + IZoraCreator1155.updateRoyaltiesForToken.selector, + newTokenId, + ICreatorRoyaltiesControl.RoyaltyConfiguration({royaltyBPS: royaltyBPS, royaltyRecipient: payoutRecipient, royaltyMintSchedule: 0}) + ); + } + function _buildCalls( uint256 newTokenId, address fixedPriceMinterAddress, @@ -261,6 +346,27 @@ library PremintTokenSetup { ); } + function _buildNewERC20SalesConfig( + address currency, + uint256 pricePerToken, + uint64 maxTokensPerAddress, + uint64 duration, + address payoutRecipient + ) private view returns (ERC20Minter.SalesConfig memory) { + uint64 saleStart = uint64(block.timestamp); + uint64 saleEnd = duration == 0 ? type(uint64).max : saleStart + duration; + + return + IERC20Minter.SalesConfig({ + saleStart: saleStart, + saleEnd: saleEnd, + maxTokensPerAddress: maxTokensPerAddress, + pricePerToken: pricePerToken, + fundsRecipient: payoutRecipient, + currency: currency + }); + } + function _buildNewSalesConfig( uint96 pricePerToken, uint64 maxTokensPerAddress, @@ -332,7 +438,7 @@ library DelegatedTokenCreation { ); (params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId); - } else { + } else if (premintVersion == PremintEncoding.HASHED_VERSION_2) { PremintConfigV2 memory premintConfig = abi.decode(premintConfigEncoded, (PremintConfigV2)); creatorAttribution = recoverCreatorAttribution( @@ -344,6 +450,20 @@ library DelegatedTokenCreation { ); (params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId); + } else if (premintVersion == PremintEncoding.HASHED_ERC20_VERSION_1) { + Erc20PremintConfigV1 memory premintConfig = abi.decode(premintConfigEncoded, (Erc20PremintConfigV1)); + + creatorAttribution = recoverCreatorAttribution( + PremintEncoding.ERC20_VERSION_1, + ZoraCreator1155Attribution.hashPremint(premintConfig), + tokenContract, + signature, + premintSignerContract + ); + + (params, tokenSetupActions) = _recoverDelegatedTokenSetup(premintConfig, newTokenId); + } else { + revert IZoraCreator1155Errors.InvalidPremintVersion(); } } @@ -352,9 +472,10 @@ library DelegatedTokenCreation { } function _supportedPremintSignatureVersions() internal pure returns (string[] memory versions) { - versions = new string[](2); + versions = new string[](3); versions[0] = PremintEncoding.VERSION_1; versions[1] = PremintEncoding.VERSION_2; + versions[2] = PremintEncoding.ERC20_VERSION_1; } function recoverCreatorAttribution( @@ -380,6 +501,21 @@ library DelegatedTokenCreation { attribution.domainName = ZoraCreator1155Attribution.NAME; } + function _recoverDelegatedTokenSetup( + Erc20PremintConfigV1 memory premintConfig, + uint256 nextTokenId + ) private view returns (DelegatedTokenSetup memory params, bytes[] memory tokenSetupActions) { + validatePremint(premintConfig.tokenConfig.mintStart, premintConfig.deleted); + + params.uid = premintConfig.uid; + + tokenSetupActions = PremintTokenSetup.makeSetupNewTokenCalls({newTokenId: nextTokenId, tokenConfig: premintConfig.tokenConfig}); + + params.tokenURI = premintConfig.tokenConfig.tokenURI; + params.maxSupply = premintConfig.tokenConfig.maxSupply; + params.createReferral = premintConfig.tokenConfig.createReferral; + } + function _recoverDelegatedTokenSetup( PremintConfigV2 memory premintConfig, uint256 nextTokenId diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol index 58dbd5d76..d1efe9616 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImpl.sol @@ -15,7 +15,8 @@ import {ERC1155DelegationStorageV1} from "../delegation/ERC1155DelegationStorage import {ZoraCreator1155PremintExecutorImplLib} from "./ZoraCreator1155PremintExecutorImplLib.sol"; import {ZoraCreator1155Attribution, DelegatedTokenCreation} from "./ZoraCreator1155Attribution.sol"; import {PremintEncoding, EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; -import {ContractCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {ContractCreationConfig, PremintConfig, PremintConfigV2, TokenCreationConfig, TokenCreationConfigV2, MintArguments, PremintResult, Erc20PremintConfigV1, Erc20TokenCreationConfigV1} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {ZoraCreator1155Attribution, DelegatedTokenCreation} from "./ZoraCreator1155Attribution.sol"; import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155PremintExecutor.sol"; import {IZoraCreator1155DelegatedCreationLegacy, IHasSupportedPremintSignatureVersions} from "../interfaces/IZoraCreator1155DelegatedCreation.sol"; import {ZoraCreator1155FactoryImpl} from "../factory/ZoraCreator1155FactoryImpl.sol"; @@ -47,6 +48,56 @@ contract ZoraCreator1155PremintExecutorImpl is __UUPSUpgradeable_init(); } + /// @notice Executes the creation of an 1155 contract, token, and/or ERC20 sale signed by a creator, and mints the first tokens to the executor of this transaction. + /// To mint the first token(s) of an ERC20 sale, the executor must approve this contract the quantity * price of the mint. + /// @dev For use with v3 of premint config, PremintConfig3, which supports ERC20 mints. + /// @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 mintRecipient + /// @param mintArguments mint arguments specifying the token mint recipient, mint comment, and mint referral + /// @param signerContract If a smart wallet was used to create the premint, the address of that smart wallet. Otherwise, set to address(0) + function premintErc20V1( + ContractCreationConfig calldata contractConfig, + Erc20PremintConfigV1 calldata premintConfig, + bytes calldata signature, + uint256 quantityToMint, + MintArguments calldata mintArguments, + address firstMinter, + address signerContract + ) external returns (PremintResult memory result) { + result = ZoraCreator1155PremintExecutorImplLib.getOrCreateContractAndToken( + zora1155Factory, + contractConfig, + PremintEncoding.encodePremintErc20V1(premintConfig), + signature, + firstMinter, + signerContract + ); + + if (quantityToMint > 0) { + ZoraCreator1155PremintExecutorImplLib.performERC20Mint( + premintConfig.tokenConfig.erc20Minter, + premintConfig.tokenConfig.currency, + premintConfig.tokenConfig.pricePerToken, + quantityToMint, + result, + mintArguments + ); + } + + { + emit PremintedV2({ + contractAddress: result.contractAddress, + tokenId: result.tokenId, + createdNewContract: result.createdNewContract, + uid: premintConfig.uid, + minter: firstMinter, + quantityMinted: quantityToMint + }); + } + } + /// @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. /// For use for EIP-1271 based signatures, where there is a signer contract. /// If the erc1155 contract hasn't been created yet, it will be created with the given config within this same transaction. diff --git a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol index 457e7bc05..b21940851 100644 --- a/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol +++ b/packages/1155-contracts/src/delegation/ZoraCreator1155PremintExecutorImplLib.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; -import {ContractCreationConfig, PremintConfig} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {ContractCreationConfig, PremintConfig, PremintResult, MintArguments} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Minter} from "../interfaces/IERC20Minter.sol"; import {IZoraCreator1155} from "../interfaces/IZoraCreator1155.sol"; import {IZoraCreator1155Factory} from "../interfaces/IZoraCreator1155Factory.sol"; import {ICreatorRoyaltiesControl} from "../interfaces/ICreatorRoyaltiesControl.sol"; @@ -10,7 +12,7 @@ import {IZoraCreator1155PremintExecutor} from "../interfaces/IZoraCreator1155Pre import {IZoraCreator1155DelegatedCreation, IZoraCreator1155DelegatedCreationLegacy, ISupportsAABasedDelegatedTokenCreation} from "../interfaces/IZoraCreator1155DelegatedCreation.sol"; import {EncodedPremintConfig} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; import {IMintWithRewardsRecipients} from "../interfaces/IMintWithRewardsRecipients.sol"; -import {MintArguments, PremintResult} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {IERC20Minter} from "../interfaces/IERC20Minter.sol"; interface ILegacyZoraCreator1155DelegatedMinter { function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature, address sender) external returns (uint256 newTokenId); @@ -124,6 +126,42 @@ library ZoraCreator1155PremintExecutorImplLib { } } + function performERC20Mint( + address erc20Minter, + address currency, + uint256 pricePerToken, + uint256 quantityToMint, + PremintResult memory premintResult, + MintArguments memory mintArguments + ) internal { + if (quantityToMint != 0) { + address mintReferral = mintArguments.mintRewardsRecipients.length > 0 ? mintArguments.mintRewardsRecipients[0] : address(0); + + uint256 totalValue = pricePerToken * quantityToMint; + + uint256 beforeBalance = IERC20(currency).balanceOf(address(this)); + IERC20(currency).transferFrom(msg.sender, address(this), totalValue); + uint256 afterBalance = IERC20(currency).balanceOf(address(this)); + + if ((beforeBalance + totalValue) != afterBalance) { + revert IERC20Minter.ERC20TransferSlippage(); + } + + IERC20(currency).approve(erc20Minter, totalValue); + + IERC20Minter(erc20Minter).mint( + mintArguments.mintRecipient, + quantityToMint, + premintResult.contractAddress, + premintResult.tokenId, + totalValue, + currency, + mintReferral, + mintArguments.mintComment + ); + } + } + function mintWithEth( IZoraCreator1155 tokenContract, address fixedPriceMinter, diff --git a/packages/1155-contracts/src/interfaces/IERC20Minter.sol b/packages/1155-contracts/src/interfaces/IERC20Minter.sol new file mode 100644 index 000000000..d4a624562 --- /dev/null +++ b/packages/1155-contracts/src/interfaces/IERC20Minter.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IERC20Minter { + struct RewardsSettings { + /// @notice Amount of the create referral reward + uint256 createReferralReward; + /// @notice Amount of the mint referral reward + uint256 mintReferralReward; + /// @notice Amount of the zora reward + uint256 zoraReward; + /// @notice Amount of the first minter reward + uint256 firstMinterReward; + } + + struct SalesConfig { + /// @notice Unix timestamp for the sale start + uint64 saleStart; + /// @notice Unix timestamp for the sale end + uint64 saleEnd; + /// @notice Max tokens that can be minted for an address, 0 if unlimited + uint64 maxTokensPerAddress; + /// @notice Price per token in ERC20 currency + uint256 pricePerToken; + /// @notice Funds recipient (0 if no different funds recipient than the contract global) + address fundsRecipient; + /// @notice ERC20 Currency address + address currency; + } + + /// @notice Rewards Deposit Event + /// @param createReferral Creator referral address + /// @param mintReferral Mint referral address + /// @param firstMinter First minter address + /// @param zora ZORA recipient address + /// @param collection The collection address of the token + /// @param currency Currency used for the deposit + /// @param tokenId Token ID + /// @param createReferralReward Creator referral reward + /// @param mintReferralReward Mint referral amount + /// @param firstMinterReward First minter amount + /// @param zoraReward ZORA amount + event ERC20RewardsDeposit( + address indexed createReferral, + address indexed mintReferral, + address indexed firstMinter, + address zora, + address collection, + address currency, + uint256 tokenId, + uint256 createReferralReward, + uint256 mintReferralReward, + uint256 firstMinterReward, + uint256 zoraReward + ); + + /// @notice ERC20MinterInitialized Event + /// @param rewardPercentage The reward percentage + event ERC20MinterInitialized(uint256 rewardPercentage); + + /// @notice MintComment Event + /// @param sender The sender of the comment + /// @param tokenContract The token contract address + /// @param tokenId The token ID + /// @param quantity The quantity of tokens minted + /// @param comment The comment + event MintComment(address indexed sender, address indexed tokenContract, uint256 indexed tokenId, uint256 quantity, string comment); + + /// @notice SaleSet Event + /// @param mediaContract The media contract address + /// @param tokenId The token ID + /// @param salesConfig The sales configuration + event SaleSet(address indexed mediaContract, uint256 indexed tokenId, SalesConfig salesConfig); + + /// @notice ZoraRewardsRecipientSet Event + /// @param prevRecipient The previous recipient address + /// @param newRecipient The new recipient address + event ZoraRewardsRecipientSet(address indexed prevRecipient, address indexed newRecipient); + + /// @notice Cannot set address to zero + error AddressZero(); + + /// @notice Cannot set currency address to zero + error InvalidCurrency(); + + /// @notice Price per ERC20 token is too low + error PricePerTokenTooLow(); + + /// @notice requestMint() is not used in ERC20 minter, use mint() instead + error RequestMintInvalidUseMint(); + + /// @notice Sale has already ended + error SaleEnded(); + + /// @notice Sale has not started yet + error SaleHasNotStarted(); + + /// @notice Value sent is incorrect + error WrongValueSent(); + + /// @notice ERC20 transfer slippage + error ERC20TransferSlippage(); + + /// @notice ERC20Minter is already initialized + error AlreadyInitialized(); + + /// @notice Only the Zora rewards recipient can call this function + error OnlyZoraRewardsRecipient(); + + /// @notice Mints a token using an ERC20 currency, note the total value must have been approved prior to calling this function + /// @param mintTo The address to mint the token to + /// @param quantity The quantity of tokens to mint + /// @param tokenAddress The address of the token to mint + /// @param tokenId The ID of the token to mint + /// @param totalValue The total value of the mint + /// @param currency The address of the currency to use for the mint + /// @param mintReferral The address of the mint referral + /// @param comment The optional mint comment + function mint( + address mintTo, + uint256 quantity, + address tokenAddress, + uint256 tokenId, + uint256 totalValue, + address currency, + address mintReferral, + string calldata comment + ) external; + + /// @notice Sets the sale config for a given token + function setSale(uint256 tokenId, SalesConfig memory salesConfig) external; + + /// @notice Returns the sale config for a given token + function sale(address tokenContract, uint256 tokenId) external view returns (SalesConfig memory); +} diff --git a/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol b/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol new file mode 100644 index 000000000..b22c2a44d --- /dev/null +++ b/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IProtocolRewards} from "@zoralabs/protocol-rewards/src/interfaces/IProtocolRewards.sol"; +import {IERC20Minter} from "../../interfaces/IERC20Minter.sol"; +import {LimitedMintPerAddress} from "../../minters/utils/LimitedMintPerAddress.sol"; +import {SaleStrategy} from "../../minters/SaleStrategy.sol"; +import {ICreatorCommands} from "../../interfaces/ICreatorCommands.sol"; +import {ERC20MinterRewards} from "./ERC20MinterRewards.sol"; +import {IZora1155} from "./IZora1155.sol"; + +/* + + + ░░░░░░░░░░░░░░ + ░░▒▒░░░░░░░░░░░░░░░░░░░░ + ░░▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░ + ░░▒▒▒▒░░░░░░░░░░░░░░ ░░░░░░░░ + ░▓▓▒▒▒▒░░░░░░░░░░░░ ░░░░░░░ + ░▓▓▓▒▒▒▒░░░░░░░░░░░░ ░░░░░░░░ + ░▓▓▓▒▒▒▒░░░░░░░░░░░░░░ ░░░░░░░░░░ + ░▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░▓▓▓▓▓▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░ + ░▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░ + ░░▓▓▓▓▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░ + ░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒░░░░░░░░░▒▒▒▒▒░░ + ░░▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░ + ░░▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░ + + OURS TRULY, + + + github.com/ourzora/zora-protocol + +*/ + +/// @title ERC20Minter +/// @notice Allows for ZoraCreator Mints to be purchased using ERC20 tokens +/// @dev While this contract _looks_ like a minter, we need to be able to directly manage ERC20 tokens. Therefore, we need to establish minter permissions but instead of using the `requestMint` flow we directly request tokens to be minted in order to safely handle the incoming ERC20 tokens. +/// @author @isabellasmallcombe +contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMintPerAddress, ERC20MinterRewards { + using SafeERC20 for IERC20; + + /// @notice The address of the Zora rewards recipient + address public zoraRewardRecipientAddress; + + /// @notice The ERC20 sale configuration for a given 1155 token + /// @dev 1155 token address => 1155 token id => SalesConfig + mapping(address => mapping(uint256 => SalesConfig)) internal salesConfigs; + + /// @notice Initializes the contract with a Zora rewards recipient address + /// @dev Allows deterministic contract address, called on deploy + function initialize(address _zoraRewardRecipientAddress) external { + if (_zoraRewardRecipientAddress == address(0)) { + revert AddressZero(); + } + + if (zoraRewardRecipientAddress != address(0)) { + revert AlreadyInitialized(); + } + + zoraRewardRecipientAddress = _zoraRewardRecipientAddress; + + emit ERC20MinterInitialized(TOTAL_REWARD_PCT); + } + + /// @notice Computes the total reward value for a given amount of ERC20 tokens + /// @param totalValue The total number of ERC20 tokens + function computeTotalReward(uint256 totalValue) public pure returns (uint256) { + return (totalValue * TOTAL_REWARD_PCT) / BPS_TO_PERCENT_2_DECIMAL_PERCISION; + } + + /// @notice Computes the rewards value given an amount and a reward percentage + /// @param totalReward The total reward to be distributed + /// @param rewardPct The percentage of the reward to be distributed + function computeReward(uint256 totalReward, uint256 rewardPct) public pure returns (uint256) { + return (totalReward * rewardPct) / BPS_TO_PERCENT_8_DECIMAL_PERCISION; + } + + /// @notice Computes the rewards for an ERC20 mint + /// @param totalReward The total reward to be distributed + function computePaidMintRewards(uint256 totalReward) public pure returns (RewardsSettings memory) { + uint256 createReferralReward = computeReward(totalReward, CREATE_REFERRAL_PAID_MINT_REWARD_PCT); + uint256 mintReferralReward = computeReward(totalReward, MINT_REFERRAL_PAID_MINT_REWARD_PCT); + uint256 firstMinterReward = computeReward(totalReward, FIRST_MINTER_REWARD_PCT); + uint256 zoraReward = totalReward - (createReferralReward + mintReferralReward + firstMinterReward); + + return + RewardsSettings({ + createReferralReward: createReferralReward, + mintReferralReward: mintReferralReward, + zoraReward: zoraReward, + firstMinterReward: firstMinterReward + }); + } + + /// @notice Gets the create referral address for a given token + /// @param tokenContract The address of the token contract + /// @param tokenId The ID of the token + function getCreateReferral(address tokenContract, uint256 tokenId) public view returns (address createReferral) { + try IZora1155(tokenContract).createReferrals(tokenId) returns (address contractCreateReferral) { + createReferral = contractCreateReferral; + } catch {} + + if (createReferral == address(0)) { + createReferral = zoraRewardRecipientAddress; + } + } + + /// @notice Gets the first minter address for a given token + /// @param tokenContract The address of the token contract + /// @param tokenId The ID of the token + function getFirstMinter(address tokenContract, uint256 tokenId) public view returns (address firstMinter) { + try IZora1155(tokenContract).firstMinters(tokenId) returns (address contractFirstMinter) { + firstMinter = contractFirstMinter; + + if (firstMinter == address(0)) { + firstMinter = IZora1155(tokenContract).getCreatorRewardRecipient(tokenId); + } + } catch { + firstMinter = zoraRewardRecipientAddress; + } + } + + /// @notice Handles the incoming transfer of ERC20 tokens + /// @param currency The address of the currency to use for the mint + /// @param totalValue The total value of the mint + function _handleIncomingTransfer(address currency, uint256 totalValue) internal { + uint256 beforeBalance = IERC20(currency).balanceOf(address(this)); + IERC20(currency).safeTransferFrom(msg.sender, address(this), totalValue); + uint256 afterBalance = IERC20(currency).balanceOf(address(this)); + + if ((beforeBalance + totalValue) != afterBalance) { + revert ERC20TransferSlippage(); + } + } + + /// @notice Distributes the rewards to the appropriate addresses + /// @param totalReward The total reward to be distributed + /// @param currency The currency used for the mint + /// @param tokenId The ID of the token to mint + /// @param tokenAddress The address of the token to mint + /// @param mintReferral The address of the mint referral + function _distributeRewards(uint256 totalReward, address currency, uint256 tokenId, address tokenAddress, address mintReferral) private { + RewardsSettings memory settings = computePaidMintRewards(totalReward); + + address createReferral = getCreateReferral(tokenAddress, tokenId); + address firstMinter = getFirstMinter(tokenAddress, tokenId); + + if (mintReferral == address(0)) { + mintReferral = zoraRewardRecipientAddress; + } + + IERC20(currency).safeTransfer(createReferral, settings.createReferralReward); + IERC20(currency).safeTransfer(firstMinter, settings.firstMinterReward); + IERC20(currency).safeTransfer(mintReferral, settings.mintReferralReward); + IERC20(currency).safeTransfer(zoraRewardRecipientAddress, settings.zoraReward); + + emit ERC20RewardsDeposit( + createReferral, + mintReferral, + firstMinter, + zoraRewardRecipientAddress, + tokenAddress, + currency, + tokenId, + settings.createReferralReward, + settings.mintReferralReward, + settings.firstMinterReward, + settings.zoraReward + ); + } + + /// @notice Mints a token using an ERC20 currency, note the total value must have been approved prior to calling this function + /// @param mintTo The address to mint the token to + /// @param quantity The quantity of tokens to mint + /// @param tokenAddress The address of the token to mint + /// @param tokenId The ID of the token to mint + /// @param totalValue The total value of the mint + /// @param currency The address of the currency to use for the mint + /// @param mintReferral The address of the mint referral + /// @param comment The optional mint comment + function mint( + address mintTo, + uint256 quantity, + address tokenAddress, + uint256 tokenId, + uint256 totalValue, + address currency, + address mintReferral, + string calldata comment + ) external nonReentrant { + SalesConfig storage config = salesConfigs[tokenAddress][tokenId]; + + if (config.currency == address(0) || config.currency != currency) { + revert InvalidCurrency(); + } + + if (totalValue != (config.pricePerToken * quantity)) { + revert WrongValueSent(); + } + + if (block.timestamp < config.saleStart) { + revert SaleHasNotStarted(); + } + + if (block.timestamp > config.saleEnd) { + revert SaleEnded(); + } + + if (config.maxTokensPerAddress > 0) { + _requireMintNotOverLimitAndUpdate(config.maxTokensPerAddress, quantity, tokenAddress, tokenId, mintTo); + } + + _handleIncomingTransfer(currency, totalValue); + + IZora1155(tokenAddress).adminMint(mintTo, tokenId, quantity, ""); + + uint256 totalReward = computeTotalReward(totalValue); + + _distributeRewards(totalReward, currency, tokenId, tokenAddress, mintReferral); + + IERC20(config.currency).safeTransfer(config.fundsRecipient, totalValue - totalReward); + + if (bytes(comment).length > 0) { + emit MintComment(mintTo, tokenAddress, tokenId, quantity, comment); + } + } + + /// @notice The percentage of the total value that is distributed as rewards + function totalRewardPct() external pure returns (uint256) { + return TOTAL_REWARD_PCT; + } + + /// @notice The URI of the contract + function contractURI() external pure returns (string memory) { + return "https://github.com/ourzora/zora-protocol/"; + } + + /// @notice The name of the contract + function contractName() external pure returns (string memory) { + return "ERC20 Minter"; + } + + /// @notice The version of the contract + function contractVersion() external pure returns (string memory) { + return "1.0.0"; + } + + /// @notice Sets the sale config for a given token + function setSale(uint256 tokenId, SalesConfig memory salesConfig) external { + if (salesConfig.pricePerToken < MIN_PRICE_PER_TOKEN) { + revert PricePerTokenTooLow(); + } + if (salesConfig.currency == address(0)) { + revert AddressZero(); + } + if (salesConfig.fundsRecipient == address(0)) { + revert AddressZero(); + } + + salesConfigs[msg.sender][tokenId] = salesConfig; + + // Emit event + emit SaleSet(msg.sender, tokenId, salesConfig); + } + + /// @notice Deletes the sale config for a given token + function resetSale(uint256 tokenId) external override { + delete salesConfigs[msg.sender][tokenId]; + + // Deleted sale emit event + emit SaleSet(msg.sender, tokenId, salesConfigs[msg.sender][tokenId]); + } + + /// @notice Returns the sale config for a given token + function sale(address tokenContract, uint256 tokenId) external view returns (SalesConfig memory) { + return salesConfigs[tokenContract][tokenId]; + } + + /// @notice IERC165 interface support + function supportsInterface(bytes4 interfaceId) public pure virtual override(LimitedMintPerAddress, SaleStrategy) returns (bool) { + return super.supportsInterface(interfaceId) || LimitedMintPerAddress.supportsInterface(interfaceId) || SaleStrategy.supportsInterface(interfaceId); + } + + /// @notice Reverts as `requestMint` is not used in the ERC20 minter. Call `mint` instead. + function requestMint(address, uint256, uint256, uint256, bytes calldata) external pure returns (ICreatorCommands.CommandSet memory) { + revert RequestMintInvalidUseMint(); + } + + /// @notice Set the Zora rewards recipient address + /// @param recipient The new recipient address + function setZoraRewardsRecipient(address recipient) external { + if (msg.sender != zoraRewardRecipientAddress) { + revert OnlyZoraRewardsRecipient(); + } + + if (recipient == address(0)) { + revert AddressZero(); + } + + emit ZoraRewardsRecipientSet(zoraRewardRecipientAddress, recipient); + + zoraRewardRecipientAddress = recipient; + } +} diff --git a/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol b/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol new file mode 100644 index 000000000..6a1509efc --- /dev/null +++ b/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @notice ERC20Minter Helper contract template +abstract contract ERC20MinterRewards { + uint256 internal constant MIN_PRICE_PER_TOKEN = 10_000; + uint256 internal constant TOTAL_REWARD_PCT = 5; // 5% + uint256 internal constant BPS_TO_PERCENT_2_DECIMAL_PERCISION = 100; + uint256 internal constant BPS_TO_PERCENT_8_DECIMAL_PERCISION = 100_000_000; + uint256 internal constant CREATE_REFERRAL_PAID_MINT_REWARD_PCT = 28_571400; // 28.5714%, roughly 0.000222 ETH at a 0.000777 value + uint256 internal constant MINT_REFERRAL_PAID_MINT_REWARD_PCT = 28_571400; // 28.5714%, roughly 0.000222 ETH at a 0.000777 value + uint256 internal constant ZORA_PAID_MINT_REWARD_PCT = 28_571400; // 28.5714%, roughly 0.000222 ETH at a 0.000777 value + uint256 internal constant FIRST_MINTER_REWARD_PCT = 14_228500; // 14.2285%, roughly 0.000111 ETH at a 0.000777 value +} diff --git a/packages/1155-contracts/src/minters/erc20/IZora1155.sol b/packages/1155-contracts/src/minters/erc20/IZora1155.sol new file mode 100644 index 000000000..b8d59a9e0 --- /dev/null +++ b/packages/1155-contracts/src/minters/erc20/IZora1155.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +/// @notice The set of public functions on a Zora 1155 contract that are called by the ERC20 minter contract +interface IZora1155 { + function createReferrals(uint256 tokenId) external view returns (address); + + function firstMinters(uint256 tokenId) external view returns (address); + + function getCreatorRewardRecipient(uint256 tokenId) external view returns (address); + + function adminMint(address recipient, uint256 tokenId, uint256 quantity, bytes memory data) external; +} diff --git a/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol b/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol new file mode 100644 index 000000000..0409ee62b --- /dev/null +++ b/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; +import {ZoraCreator1155Impl} from "../../../src/nft/ZoraCreator1155Impl.sol"; +import {Zora1155} from "../../../src/proxies/Zora1155.sol"; +import {IMinter1155} from "../../../src/interfaces/IMinter1155.sol"; +import {ICreatorRoyaltiesControl} from "../../../src/interfaces/ICreatorRoyaltiesControl.sol"; +import {ILimitedMintPerAddressErrors} from "../../../src/interfaces/ILimitedMintPerAddress.sol"; +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {ERC20Minter} from "../../../src/minters/erc20/ERC20Minter.sol"; +import {IERC20Minter} from "../../../src/interfaces/IERC20Minter.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IZoraCreator1155Errors} from "../../../src/interfaces/IZoraCreator1155Errors.sol"; + +contract ERC20MinterTest is Test { + ZoraCreator1155Impl internal target; + ERC20PresetMinterPauser currency; + address payable internal admin = payable(address(0x999)); + address internal zora; + address internal tokenRecipient; + address internal fundsRecipient; + address internal createReferral; + address internal mintReferral; + ERC20Minter internal minter; + + uint256 internal constant TOTAL_REWARD_PCT = 5; + uint256 immutable BPS_TO_PERCENT = 100; + uint256 internal constant CREATE_REFERRAL_PAID_MINT_REWARD_PCT = 28_571400; + uint256 internal constant MINT_REFERRAL_PAID_MINT_REWARD_PCT = 28_571400; + uint256 internal constant ZORA_PAID_MINT_REWARD_PCT = 28_571400; + uint256 internal constant FIRST_MINTER_REWARD_PCT = 14_228500; + uint256 immutable BPS_TO_PERCENT_8_DECIMAL_PERCISION = 100_000_000; + + event ERC20RewardsDeposit( + address indexed createReferral, + address indexed mintReferral, + address indexed firstMinter, + address zora, + address collection, + address currency, + uint256 tokenId, + uint256 createReferralReward, + uint256 mintReferralReward, + uint256 firstMinterReward, + uint256 zoraReward + ); + + event ERC20MinterInitialized(uint256 rewardPercentage); + + event MintComment(address indexed sender, address indexed tokenContract, uint256 indexed tokenId, uint256 quantity, string comment); + + function setUp() external { + zora = makeAddr("zora"); + tokenRecipient = makeAddr("tokenRecipient"); + fundsRecipient = makeAddr("fundsRecipient"); + createReferral = makeAddr("createReferral"); + mintReferral = makeAddr("mintReferral"); + + bytes[] memory emptyData = new bytes[](0); + ProtocolRewards protocolRewards = new ProtocolRewards(); + ZoraCreator1155Impl targetImpl = new ZoraCreator1155Impl(zora, address(0), address(protocolRewards)); + Zora1155 proxy = new Zora1155(address(targetImpl)); + target = ZoraCreator1155Impl(payable(address(proxy))); + target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, emptyData); + minter = new ERC20Minter(); + minter.initialize(zora); + vm.prank(admin); + currency = new ERC20PresetMinterPauser("Test currency", "TEST"); + } + + function setUpTargetSale(uint256 price, address tokenFundsRecipient, address tokenCurrency, uint256 quantity) internal returns (uint256) { + vm.startPrank(admin); + uint256 newTokenId = target.setupNewTokenWithCreateReferral("https://zora.co/testing/token.json", quantity, createReferral); + target.addPermission(newTokenId, address(minter), target.PERMISSION_BIT_MINTER()); + target.callSale( + newTokenId, + minter, + abi.encodeWithSelector( + ERC20Minter.setSale.selector, + newTokenId, + IERC20Minter.SalesConfig({ + pricePerToken: price, + saleStart: 0, + saleEnd: type(uint64).max, + maxTokensPerAddress: 0, + fundsRecipient: tokenFundsRecipient, + currency: tokenCurrency + }) + ) + ); + vm.stopPrank(); + + return newTokenId; + } + + function test_ERC20MinterInitializeEventIsEmitted() external { + vm.expectEmit(true, true, true, true); + emit ERC20MinterInitialized(TOTAL_REWARD_PCT); + minter = new ERC20Minter(); + minter.initialize(zora); + } + + function test_ERC20MinterCannotInitializeWithAddressZero() external { + minter = new ERC20Minter(); + + vm.expectRevert(abi.encodeWithSignature("AddressZero()")); + minter.initialize(address(0)); + } + + function test_ERC20MinterCannotReinitialize() external { + vm.expectRevert(abi.encodeWithSignature("AlreadyInitialized()")); + minter.initialize(address(this)); + } + + function test_ERC20MinterContractName() external { + assertEq(minter.contractName(), "ERC20 Minter"); + } + + function test_ERC20MinterContractVersion() external { + assertEq(minter.contractVersion(), "1.0.0"); + } + + function test_ERC20MinterSaleConfigPriceTooLow() external { + vm.startPrank(admin); + uint256 newTokenId = target.setupNewToken("https://zora.co/testing/token.json", 10); + target.addPermission(newTokenId, address(minter), target.PERMISSION_BIT_MINTER()); + + bytes memory minterError = abi.encodeWithSignature("PricePerTokenTooLow()"); + vm.expectRevert(abi.encodeWithSignature("CallFailed(bytes)", minterError)); + target.callSale( + newTokenId, + minter, + abi.encodeWithSelector( + ERC20Minter.setSale.selector, + newTokenId, + IERC20Minter.SalesConfig({ + pricePerToken: 1, + saleStart: 0, + saleEnd: type(uint64).max, + maxTokensPerAddress: 0, + fundsRecipient: address(0), + currency: address(currency) + }) + ) + ); + vm.stopPrank(); + } + + function test_ERC20MinterRevertIfFundsRecipientAddressZero() external { + vm.startPrank(admin); + uint256 newTokenId = target.setupNewTokenWithCreateReferral("https://zora.co/testing/token.json", 1, createReferral); + target.addPermission(newTokenId, address(minter), target.PERMISSION_BIT_MINTER()); + + bytes memory minterError = abi.encodeWithSignature("AddressZero()"); + vm.expectRevert(abi.encodeWithSignature("CallFailed(bytes)", minterError)); + target.callSale( + newTokenId, + minter, + abi.encodeWithSelector( + ERC20Minter.setSale.selector, + newTokenId, + IERC20Minter.SalesConfig({ + pricePerToken: 10_000, + saleStart: 0, + saleEnd: type(uint64).max, + maxTokensPerAddress: 0, + fundsRecipient: address(0), + currency: address(currency) + }) + ) + ); + vm.stopPrank(); + } + + function test_ERC20MinterRevertIfCurrencyZero() external { + vm.startPrank(admin); + uint256 newTokenId = target.setupNewTokenWithCreateReferral("https://zora.co/testing/token.json", 1, createReferral); + target.addPermission(newTokenId, address(minter), target.PERMISSION_BIT_MINTER()); + + bytes memory minterError = abi.encodeWithSignature("AddressZero()"); + vm.expectRevert(abi.encodeWithSignature("CallFailed(bytes)", minterError)); + target.callSale( + newTokenId, + minter, + abi.encodeWithSelector( + ERC20Minter.setSale.selector, + newTokenId, + IERC20Minter.SalesConfig({ + pricePerToken: 10_000, + saleStart: 0, + saleEnd: type(uint64).max, + maxTokensPerAddress: 0, + fundsRecipient: fundsRecipient, + currency: address(0) + }) + ) + ); + vm.stopPrank(); + } + + function test_ERC20MinterRevertIfCurrencyDoesNotMatchSalesConfigCurrency() external { + setUpTargetSale(10_000, fundsRecipient, address(currency), 1); + + vm.expectRevert(abi.encodeWithSignature("InvalidCurrency()")); + minter.mint(tokenRecipient, 1, address(target), 1, 1, makeAddr("0x123"), address(0), ""); + } + + function test_ERC20MinterRequestMintInvalid() external { + vm.expectRevert(abi.encodeWithSignature("RequestMintInvalidUseMint()")); + minter.requestMint(address(0), 1, 1, 1, ""); + } + + function test_ERC20MinterComputePaidMintRewards() external { + uint256 totalValue = 500000000000000000; // 0.5 when converted from wei + ERC20Minter.RewardsSettings memory rewardsSettings = minter.computePaidMintRewards(totalValue); + + assertEq(rewardsSettings.createReferralReward, 142857000000000000); + assertEq(rewardsSettings.mintReferralReward, 142857000000000000); + assertEq(rewardsSettings.firstMinterReward, 71142500000000000); + assertEq(rewardsSettings.zoraReward, 143143500000000000); + assertEq( + rewardsSettings.createReferralReward + rewardsSettings.mintReferralReward + rewardsSettings.zoraReward + rewardsSettings.firstMinterReward, + totalValue + ); + } + + function test_ERC20MinterSaleFlow() external { + uint96 pricePerToken = 10_000; + uint256 quantity = 2; + uint256 newTokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + + vm.deal(tokenRecipient, 1 ether); + vm.prank(admin); + uint256 totalValue = pricePerToken * quantity; + currency.mint(address(tokenRecipient), totalValue); + + vm.prank(tokenRecipient); + currency.approve(address(minter), totalValue); + + vm.startPrank(tokenRecipient); + minter.mint(tokenRecipient, quantity, address(target), newTokenId, pricePerToken * quantity, address(currency), mintReferral, ""); + vm.stopPrank(); + + assertEq(target.balanceOf(tokenRecipient, newTokenId), quantity); + assertEq(currency.balanceOf(fundsRecipient), 19000); + assertEq(currency.balanceOf(address(zora)), 288); + assertEq(currency.balanceOf(mintReferral), 285); + assertEq(currency.balanceOf(admin), 142); + assertEq(currency.balanceOf(createReferral), 285); + assertEq( + currency.balanceOf(address(zora)) + + currency.balanceOf(fundsRecipient) + + currency.balanceOf(mintReferral) + + currency.balanceOf(admin) + + currency.balanceOf(createReferral), + totalValue + ); + } + + function test_ERC20MinterSaleWithRewardsAddresses() external { + uint96 pricePerToken = 100000000000000000; // 0.1 when converted from wei + uint256 quantity = 5; + uint256 newTokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + + vm.deal(tokenRecipient, 1 ether); + vm.prank(admin); + uint256 totalValue = pricePerToken * quantity; + currency.mint(address(tokenRecipient), totalValue); + + vm.prank(tokenRecipient); + currency.approve(address(minter), totalValue); + + vm.startPrank(tokenRecipient); + minter.mint(tokenRecipient, quantity, address(target), newTokenId, pricePerToken * quantity, address(currency), mintReferral, ""); + vm.stopPrank(); + + assertEq(target.balanceOf(tokenRecipient, newTokenId), quantity); + assertEq(currency.balanceOf(fundsRecipient), 475000000000000000); + assertEq(currency.balanceOf(address(zora)), 7157175000000000); + assertEq(currency.balanceOf(createReferral), 7142850000000000); + assertEq(currency.balanceOf(mintReferral), 7142850000000000); + assertEq( + currency.balanceOf(address(zora)) + + currency.balanceOf(fundsRecipient) + + currency.balanceOf(createReferral) + + currency.balanceOf(mintReferral) + + currency.balanceOf(admin), + totalValue + ); + } + + function test_ERC20MinterSaleFuzz(uint96 pricePerToken, uint256 quantity) external { + vm.assume(quantity > 0 && quantity < 1_000_000_000); + vm.assume(pricePerToken > 10_000 && pricePerToken < type(uint96).max); + + uint256 tokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + + vm.deal(tokenRecipient, 1 ether); + vm.prank(admin); + uint256 totalValue = pricePerToken * quantity; + currency.mint(address(tokenRecipient), totalValue); + + vm.prank(tokenRecipient); + currency.approve(address(minter), totalValue); + + uint256 reward = (totalValue * TOTAL_REWARD_PCT) / BPS_TO_PERCENT; + uint256 createReferralReward = (reward * CREATE_REFERRAL_PAID_MINT_REWARD_PCT) / BPS_TO_PERCENT_8_DECIMAL_PERCISION; + uint256 mintReferralReward = (reward * MINT_REFERRAL_PAID_MINT_REWARD_PCT) / BPS_TO_PERCENT_8_DECIMAL_PERCISION; + uint256 firstMinterReward = (reward * FIRST_MINTER_REWARD_PCT) / BPS_TO_PERCENT_8_DECIMAL_PERCISION; + uint256 zoraReward = reward - (createReferralReward + mintReferralReward + firstMinterReward); + + vm.startPrank(tokenRecipient); + vm.expectEmit(true, true, true, true); + emit ERC20RewardsDeposit( + createReferral, + mintReferral, + address(admin), + zora, + address(target), + address(currency), + tokenId, + createReferralReward, + mintReferralReward, + firstMinterReward, + zoraReward + ); + minter.mint(tokenRecipient, quantity, address(target), tokenId, pricePerToken * quantity, address(currency), mintReferral, ""); + vm.stopPrank(); + + assertEq(target.balanceOf(tokenRecipient, tokenId), quantity); + assertEq(currency.balanceOf(address(zora)), zoraReward); + assertEq(currency.balanceOf(createReferral), createReferralReward); + assertEq(currency.balanceOf(mintReferral), mintReferralReward); + assertEq(currency.balanceOf(admin), firstMinterReward); + assertEq(currency.balanceOf(address(zora)) + currency.balanceOf(mintReferral) + currency.balanceOf(admin) + currency.balanceOf(createReferral), reward); + assertEq( + currency.balanceOf(address(zora)) + + currency.balanceOf(fundsRecipient) + + currency.balanceOf(createReferral) + + currency.balanceOf(mintReferral) + + currency.balanceOf(admin), + totalValue + ); + } + + function test_ERC20MinterCreateReferral() public { + vm.startPrank(admin); + uint256 newTokenId = target.setupNewTokenWithCreateReferral("https://zora.co/testing/token.json", 1, createReferral); + target.addPermission(newTokenId, address(minter), target.PERMISSION_BIT_MINTER()); + vm.stopPrank(); + + address targetCreateReferral = minter.getCreateReferral(address(target), newTokenId); + assertEq(targetCreateReferral, createReferral); + + address fallbackCreateReferral = minter.getCreateReferral(address(this), 1); + assertEq(fallbackCreateReferral, minter.zoraRewardRecipientAddress()); + } + + function test_ERC20MinterFirstMinterFallback() public { + uint256 pricePerToken = 1e18; + uint256 quantity = 11; + uint256 totalValue = pricePerToken * quantity; + + uint256 tokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + address collector = makeAddr("collector"); + + vm.prank(admin); + currency.mint(collector, totalValue); + + vm.startPrank(collector); + currency.approve(address(minter), totalValue); + minter.mint(collector, quantity, address(target), tokenId, totalValue, address(currency), address(0), ""); + vm.stopPrank(); + + address firstMinter = minter.getFirstMinter(address(target), tokenId); + assertEq(firstMinter, admin); + + address fallbackFirstMinter = minter.getFirstMinter(address(this), 1); + assertEq(fallbackFirstMinter, minter.zoraRewardRecipientAddress()); + } + + function test_ERC20MinterSetZoraRewardsRecipient() public { + vm.prank(zora); + minter.setZoraRewardsRecipient(address(this)); + + assertEq(minter.zoraRewardRecipientAddress(), address(this)); + } + + function test_ERC20MinterOnlyRecipientAddressCanSet() public { + vm.expectRevert(abi.encodeWithSignature("OnlyZoraRewardsRecipient()")); + minter.setZoraRewardsRecipient(address(this)); + } + + function test_ERC20MinterCannotSetRecipientToZero() public { + vm.expectRevert(abi.encodeWithSignature("AddressZero()")); + vm.prank(zora); + minter.setZoraRewardsRecipient(address(0)); + } +} diff --git a/packages/1155-contracts/test/premint/PremintERC20.t.sol b/packages/1155-contracts/test/premint/PremintERC20.t.sol new file mode 100644 index 000000000..7dbdaff9f --- /dev/null +++ b/packages/1155-contracts/test/premint/PremintERC20.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "forge-std/Test.sol"; +import "forge-std/console2.sol"; + +import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; +import {ProtocolRewards} from "@zoralabs/protocol-rewards/src/ProtocolRewards.sol"; +import {UpgradeGate} from "../../src/upgrades/UpgradeGate.sol"; +import {ERC20Minter} from "../../src/minters/erc20/ERC20Minter.sol"; +import {IMinter1155} from "../../src/interfaces/IMinter1155.sol"; +import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; +import {IZoraCreator1155PremintExecutor, ZoraCreator1155PremintExecutorImpl} from "../../src/delegation/ZoraCreator1155PremintExecutorImpl.sol"; +import {ZoraCreator1155PremintExecutorImplLib} from "../../src/delegation/ZoraCreator1155PremintExecutorImplLib.sol"; +import {ZoraCreator1155Attribution, ContractCreationConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {Erc20TokenCreationConfigV1, Erc20PremintConfigV1, MintArguments} from "@zoralabs/shared-contracts/entities/Premint.sol"; +import {PremintEncoding} from "@zoralabs/shared-contracts/premint/PremintEncoding.sol"; +import {ZoraCreator1155FactoryImpl} from "../../src/factory/ZoraCreator1155FactoryImpl.sol"; +import {ZoraCreator1155Impl} from "../../src/nft/ZoraCreator1155Impl.sol"; +import {Zora1155PremintExecutor} from "../../src/proxies/Zora1155PremintExecutor.sol"; +import {Zora1155Factory} from "../../src/proxies/Zora1155Factory.sol"; +import {Zora1155} from "../../src/proxies/Zora1155.sol"; + +contract PremintERC20Test is Test { + uint256 internal creatorPK; + address internal creator; + address internal zora; + address internal collector; + + ProtocolRewards internal protocolRewards; + ERC20Minter internal erc20Minter; + ERC20PresetMinterPauser internal mockErc20; + + address internal zora1155Impl; + address internal factoryImpl; + address internal premintImpl; + + ZoraCreator1155FactoryImpl internal factory; + ZoraCreator1155PremintExecutorImpl internal premint; + + function setUp() public { + (creator, creatorPK) = makeAddrAndKey("creator"); + collector = makeAddr("collector"); + zora = makeAddr("zora"); + + mockErc20 = new ERC20PresetMinterPauser("Mock", "MOCK"); + erc20Minter = new ERC20Minter(); + erc20Minter.initialize(zora); + protocolRewards = new ProtocolRewards(); + + zora1155Impl = address(new ZoraCreator1155Impl(zora, address(new UpgradeGate()), address(protocolRewards))); + factoryImpl = address( + new ZoraCreator1155FactoryImpl(IZoraCreator1155(zora1155Impl), IMinter1155(address(0)), IMinter1155(address(0)), IMinter1155(address(0))) + ); + factory = ZoraCreator1155FactoryImpl(address(new Zora1155Factory(factoryImpl, abi.encodeWithSignature("initialize(address)", zora)))); + premintImpl = address(new ZoraCreator1155PremintExecutorImpl(factory)); + premint = ZoraCreator1155PremintExecutorImpl(address(new Zora1155PremintExecutor(premintImpl, abi.encodeWithSignature("initialize(address)", zora)))); + + vm.label(address(factory), "FACTORY_CONTRACT"); + vm.label(address(premint), "PREMINT_CONTRACT"); + } + + function testPremintERC20() public { + ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"}); + + Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({ + tokenURI: "test.token.uri", + maxSupply: 1000, + royaltyBPS: 0, + payoutRecipient: collector, + createReferral: address(0), + erc20Minter: address(erc20Minter), + mintStart: 0, + mintDuration: 0, + maxTokensPerAddress: 0, + currency: address(mockErc20), + pricePerToken: 1e18 + }); + + Erc20PremintConfigV1 memory premintConfig = Erc20PremintConfigV1({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false}); + + address contractAddress = premint.getContractAddress(contractConfig); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_ERC20_VERSION_1, block.chainid); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + MintArguments memory mintArguments = MintArguments({mintRecipient: collector, mintComment: "test comment", mintRewardsRecipients: new address[](0)}); + + uint256 quantityToMint = 1; + uint256 totalValue = tokenConfig.pricePerToken * quantityToMint; + mockErc20.mint(collector, totalValue); + + vm.prank(collector); + mockErc20.approve(address(premint), totalValue); + + vm.prank(collector); + premint.premintErc20V1(contractConfig, premintConfig, signature, quantityToMint, mintArguments, collector, address(0)); + } + + function testRevertExecutorMustApproveERC20Transfer() public { + ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"}); + + Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({ + tokenURI: "test.token.uri", + maxSupply: 1000, + royaltyBPS: 0, + payoutRecipient: collector, + createReferral: address(0), + erc20Minter: address(erc20Minter), + mintStart: 0, + mintDuration: 0, + maxTokensPerAddress: 0, + currency: address(mockErc20), + pricePerToken: 1e18 + }); + + Erc20PremintConfigV1 memory premintConfig = Erc20PremintConfigV1({tokenConfig: tokenConfig, uid: 1, version: 3, deleted: false}); + + address contractAddress = premint.getContractAddress(contractConfig); + bytes32 structHash = ZoraCreator1155Attribution.hashPremint(premintConfig); + bytes32 digest = ZoraCreator1155Attribution.premintHashedTypeDataV4(structHash, contractAddress, PremintEncoding.HASHED_ERC20_VERSION_1, block.chainid); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(creatorPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + MintArguments memory mintArguments = MintArguments({mintRecipient: collector, mintComment: "test comment", mintRewardsRecipients: new address[](0)}); + + uint256 quantityToMint = 1; + uint256 totalValue = tokenConfig.pricePerToken * quantityToMint; + mockErc20.mint(collector, totalValue); + + vm.prank(collector); + vm.expectRevert("ERC20: insufficient allowance"); + premint.premintErc20V1(contractConfig, premintConfig, signature, quantityToMint, mintArguments, collector, address(0)); + } +} diff --git a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol index 636e10c37..096753cab 100644 --- a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol +++ b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -841,9 +841,10 @@ contract ZoraCreator1155PreminterTest is Test { // build a premint string[] memory supportedVersions = preminter.supportedPremintSignatureVersions(makeAddr("randomContract")); - assertEq(supportedVersions.length, 2); + assertEq(supportedVersions.length, 3); assertEq(supportedVersions[0], "1"); assertEq(supportedVersions[1], "2"); + assertEq(supportedVersions[2], "ERC20_1"); } function test_premintVersion_whenCreated_returnsAllVersion() external { @@ -858,9 +859,10 @@ contract ZoraCreator1155PreminterTest is Test { string[] memory supportedVersions = preminter.supportedPremintSignatureVersions(deterministicAddress); - assertEq(supportedVersions.length, 2); + assertEq(supportedVersions.length, 3); assertEq(supportedVersions[0], "1"); assertEq(supportedVersions[1], "2"); + assertEq(supportedVersions[2], "ERC20_1"); } function testPremintWithCreateReferral() public { diff --git a/packages/1155-deployments/addresses/1.json b/packages/1155-deployments/addresses/1.json index d0a9e8394..0a624a3d5 100644 --- a/packages/1155-deployments/addresses/1.json +++ b/packages/1155-deployments/addresses/1.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x78964965cF77850224513a367f899435C5B69174", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1706664239 } diff --git a/packages/1155-deployments/addresses/10.json b/packages/1155-deployments/addresses/10.json index 83e5d6e8f..0f8b6a362 100644 --- a/packages/1155-deployments/addresses/10.json +++ b/packages/1155-deployments/addresses/10.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x1B28A04b7eB7b93f920ddF2021aa3fAE065395f2", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1706664035 } diff --git a/packages/1155-deployments/addresses/11155111.json b/packages/1155-deployments/addresses/11155111.json index 57fb271a6..993fcb398 100644 --- a/packages/1155-deployments/addresses/11155111.json +++ b/packages/1155-deployments/addresses/11155111.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x69bB4A24EBD8b1B87AF4538E0Ca3075b7E398c3D", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1704336497 } \ No newline at end of file diff --git a/packages/1155-deployments/addresses/42161.json b/packages/1155-deployments/addresses/42161.json index de566ccca..05ccc14f6 100644 --- a/packages/1155-deployments/addresses/42161.json +++ b/packages/1155-deployments/addresses/42161.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x69bB4A24EBD8b1B87AF4538E0Ca3075b7E398c3D", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1706664653 } diff --git a/packages/1155-deployments/addresses/421614.json b/packages/1155-deployments/addresses/421614.json index f6928bb78..4f26eb6c6 100644 --- a/packages/1155-deployments/addresses/421614.json +++ b/packages/1155-deployments/addresses/421614.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x69bB4A24EBD8b1B87AF4538E0Ca3075b7E398c3D", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1706661669 } diff --git a/packages/1155-deployments/addresses/7777777.json b/packages/1155-deployments/addresses/7777777.json index 4c3f0e08b..1d832dee0 100644 --- a/packages/1155-deployments/addresses/7777777.json +++ b/packages/1155-deployments/addresses/7777777.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x78964965cF77850224513a367f899435C5B69174", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1706663694 } diff --git a/packages/1155-deployments/addresses/81457.json b/packages/1155-deployments/addresses/81457.json index 2bdd3e0bd..db485d557 100644 --- a/packages/1155-deployments/addresses/81457.json +++ b/packages/1155-deployments/addresses/81457.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x7A0dE1B1f5420Df5D946878fBe2cF109011BE614", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1709235955 } diff --git a/packages/1155-deployments/addresses/8453.json b/packages/1155-deployments/addresses/8453.json index 8537db26f..8921887ee 100644 --- a/packages/1155-deployments/addresses/8453.json +++ b/packages/1155-deployments/addresses/8453.json @@ -9,5 +9,6 @@ "PREMINTER_PROXY": "0x7777773606e7e46C8Ba8B98C08f5cD218e31d340", "REDEEM_MINTER_FACTORY": "0x78964965cF77850224513a367f899435C5B69174", "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "ERC20_MINTER": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31", "timestamp": 1706663976 } diff --git a/packages/1155-deployments/deterministicConfig/erc20Minter/config.json b/packages/1155-deployments/deterministicConfig/erc20Minter/config.json new file mode 100644 index 000000000..19e412d3c --- /dev/null +++ b/packages/1155-deployments/deterministicConfig/erc20Minter/config.json @@ -0,0 +1,5 @@ +{ + "creationCode": "0x608060405234801561001057600080fd5b506001600081905550613509806100286000396000f3fe608060405234801561001057600080fd5b50600436106100f15760003560e01c806301ffc9a7146100f657806319b45c4f146101265780634132239b146101425780635369d3e8146101725780635c046084146101905780635d759bd8146101c0578063611efc09146101f057806362b60245146102205780636890e5b31461023e57806375d0c0dc1461026e5780637b49ff2c1461028c5780638f4d174e146102bc578063a0a8e460146102d8578063ac973d5b146102f6578063c4d66de814610326578063ce35b8af14610342578063e2ebba291461035e578063e8a3d4851461038e578063f54f216a146103ac575b600080fd5b610110600480360381019061010b919061223c565b6103c8565b60405161011d9190612284565b60405180910390f35b610140600480360381019061013b91906122d5565b6103fa565b005b61015c600480360381019061015791906122d5565b610595565b6040516101699190612311565b60405180910390f35b61017a6105b7565b604051610187919061236d565b60405180910390f35b6101aa60048036038101906101a591906122d5565b6105dd565b6040516101b791906123ec565b60405180910390f35b6101da60048036038101906101d59190612433565b61066c565b6040516101e7919061236d565b60405180910390f35b61020a60048036038101906102059190612433565b61074b565b6040516102179190612520565b60405180910390f35b610228610902565b6040516102359190612311565b60405180910390f35b610258600480360381019061025391906125a0565b61090b565b604051610265919061287d565b60405180910390f35b610276610945565b60405161028391906128f4565b60405180910390f35b6102a660048036038101906102a19190612916565b610982565b6040516102b39190612311565b60405180910390f35b6102d660048036038101906102d19190612969565b610a1b565b005b6102e0610bc8565b6040516102ed91906128f4565b60405180910390f35b610310600480360381019061030b9190612433565b610c05565b60405161031d919061236d565b60405180910390f35b610340600480360381019061033b9190612969565b610d64565b005b61035c60048036038101906103579190612ae2565b610ece565b005b61037860048036038101906103739190612b22565b6111ae565b6040516103859190612311565b60405180910390f35b6103966111d3565b6040516103a391906128f4565b60405180910390f35b6103c660048036038101906103c19190612bb8565b6111f3565b005b60006103d38261162d565b806103e357506103e28261162d565b5b806103f357506103f282611697565b5b9050919050565b600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828152602001908152602001600020600080820160006101000a81549067ffffffffffffffff02191690556000820160086101000a81549067ffffffffffffffff02191690556000820160106101000a81549067ffffffffffffffff021916905560018201600090556002820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690556003820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690555050803373ffffffffffffffffffffffffffffffffffffffff167f5a10ca3dd9f8e42a050c7f379d07bacd686570baf0b1c8574362fc474a9aa1a0600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600085815260200190815260200160002060405161058a9190612e26565b60405180910390a350565b600060646005836105a69190612e70565b6105b09190612ee1565b9050919050565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6105e561210e565b60006105f5836301b3f7086111ae565b90506000610607846301b3f7086111ae565b905060006106188562d91c146111ae565b905060008183856106299190612f12565b6106339190612f12565b8661063e9190612f46565b9050604051806080016040528085815260200184815260200182815260200183815250945050505050919050565b60008273ffffffffffffffffffffffffffffffffffffffff16637dafae4d836040518263ffffffff1660e01b81526004016106a79190612311565b602060405180830381865afa9250505080156106e157506040513d601f19601f820116820180604052508101906106de9190612f8f565b60015b156106eb57809150505b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff160361074557600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505b92915050565b610753612136565b600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008381526020019081526020016000206040518060c00160405290816000820160009054906101000a900467ffffffffffffffff1667ffffffffffffffff1667ffffffffffffffff1681526020016000820160089054906101000a900467ffffffffffffffff1667ffffffffffffffff1667ffffffffffffffff1681526020016000820160109054906101000a900467ffffffffffffffff1667ffffffffffffffff1667ffffffffffffffff168152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020016003820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681525050905092915050565b60006005905090565b6109136121b6565b6040517f9274ec1d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60606040518060400160405280600c81526020017f4552433230204d696e7465720000000000000000000000000000000000000000815250905090565b6000600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600084815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490509392505050565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610aa2576040517f8ba1835100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1603610b08576040517f9fabe1c100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fc93702c5f2a62678849c2f198d92f7833a8d4a8afc765cf15693222ab5b15e5060405160405180910390a380600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60606040518060400160405280600581526020017f312e302e30000000000000000000000000000000000000000000000000000000815250905090565b60008273ffffffffffffffffffffffffffffffffffffffff16639ebb8324836040518263ffffffff1660e01b8152600401610c409190612311565b602060405180830381865afa925050508015610c7a57506040513d601f19601f82011682018060405250810190610c779190612f8f565b60015b610ca857600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050610d5e565b809150600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610d5c578373ffffffffffffffffffffffffffffffffffffffff16635e4e0404846040518263ffffffff1660e01b8152600401610d189190612311565b602060405180830381865afa158015610d35573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d599190612f8f565b91505b505b92915050565b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1603610dca576040517f9fabe1c100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614610e52576040517f0dc149f000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507f658c97f8591190c843529a9019f5ae8055b6c690ab14156b0e589a5253c5ccb96005604051610ec39190612311565b60405180910390a150565b61271081606001511015610f0e576040517f835de65800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168160a0015173ffffffffffffffffffffffffffffffffffffffff1603610f78576040517f9fabe1c100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff16816080015173ffffffffffffffffffffffffffffffffffffffff1603610fe2576040517f9fabe1c100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600084815260200190815260200160002060008201518160000160006101000a81548167ffffffffffffffff021916908367ffffffffffffffff16021790555060208201518160000160086101000a81548167ffffffffffffffff021916908367ffffffffffffffff16021790555060408201518160000160106101000a81548167ffffffffffffffff021916908367ffffffffffffffff1602179055506060820151816001015560808201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060a08201518160030160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550905050813373ffffffffffffffffffffffffffffffffffffffff167f5a10ca3dd9f8e42a050c7f379d07bacd686570baf0b1c8574362fc474a9aa1a0836040516111a29190612520565b60405180910390a35050565b60006305f5e10082846111c19190612e70565b6111cb9190612ee1565b905092915050565b60606040518060600160405280602981526020016134ab60299139905090565b6111fb611769565b6000600360008973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008881526020019081526020016000209050600073ffffffffffffffffffffffffffffffffffffffff168160030160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614806112fe57508473ffffffffffffffffffffffffffffffffffffffff168160030160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614155b15611335576040517ff599342800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8881600101546113459190612e70565b861461137d576040517f2f4613eb00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8060000160009054906101000a900467ffffffffffffffff1667ffffffffffffffff164210156113d9576040517fe8c4db8200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8060000160089054906101000a900467ffffffffffffffff1667ffffffffffffffff16421115611435576040517f0bd8a3eb00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008160000160109054906101000a900467ffffffffffffffff1667ffffffffffffffff1611156114905761148f8160000160109054906101000a900467ffffffffffffffff1667ffffffffffffffff168a8a8a8e6117b8565b5b61149a85876119c6565b8773ffffffffffffffffffffffffffffffffffffffff1663c238d1ee8b898c6040518463ffffffff1660e01b81526004016114d793929190612ff3565b600060405180830381600087803b1580156114f157600080fd5b505af1158015611505573d6000803e3d6000fd5b50505050600061151487610595565b905061152381878a8c89611b39565b6115a18260020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1682896115579190612f46565b8460030160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16611d609092919063ffffffff16565b600084849050111561161857878973ffffffffffffffffffffffffffffffffffffffff168c73ffffffffffffffffffffffffffffffffffffffff167fb9490aee663998179ad13f9e1c1eb6189c71ad1a9ec87f33ad2766f98d9a268a8d888860405161160f93929190613079565b60405180910390a45b5050611622611de6565b505050505050505050565b60007f7b49ff2c000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916149050919050565b60007f6890e5b3000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916148061176257507f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916145b9050919050565b6002600054036117ae576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016117a5906130f7565b60405180910390fd5b6002600081905550565b83600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600084815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546118559190612f12565b9250508190555084600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600084815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205411156119bf578085600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600085815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040517fe2d915640000000000000000000000000000000000000000000000000000000081526004016119b693929190613117565b60405180910390fd5b5050505050565b60008273ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b8152600401611a01919061236d565b602060405180830381865afa158015611a1e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611a429190613163565b9050611a713330848673ffffffffffffffffffffffffffffffffffffffff16611df0909392919063ffffffff16565b60008373ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b8152600401611aac919061236d565b602060405180830381865afa158015611ac9573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611aed9190613163565b9050808383611afc9190612f12565b14611b33576040517fdc5b522d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50505050565b6000611b44866105dd565b90506000611b52848661066c565b90506000611b608587610c05565b9050600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1603611bbc57600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1693505b611beb8284600001518973ffffffffffffffffffffffffffffffffffffffff16611d609092919063ffffffff16565b611c1a8184606001518973ffffffffffffffffffffffffffffffffffffffff16611d609092919063ffffffff16565b611c498484602001518973ffffffffffffffffffffffffffffffffffffffff16611d609092919063ffffffff16565b611c9a600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1684604001518973ffffffffffffffffffffffffffffffffffffffff16611d609092919063ffffffff16565b8073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f6d43fd9d85f88450d1b777b5e7d58dec9b8dd36d77bf01bc5e5423d4a92d2246600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16898c8c8a600001518b602001518c606001518d60400151604051611d4e989796959493929190613190565b60405180910390a45050505050505050565b611de18363a9059cbb60e01b8484604051602401611d7f92919061320e565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050611e79565b505050565b6001600081905550565b611e73846323b872dd60e01b858585604051602401611e1193929190613237565b604051602081830303815290604052907bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050611e79565b50505050565b6000611edb826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff16611f419092919063ffffffff16565b9050600081511480611efd575080806020019051810190611efc919061329a565b5b611f3c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611f3390613339565b60405180910390fd5b505050565b6060611f508484600085611f59565b90509392505050565b606082471015611f9e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611f95906133cb565b60405180910390fd5b6000808673ffffffffffffffffffffffffffffffffffffffff168587604051611fc79190613427565b60006040518083038185875af1925050503d8060008114612004576040519150601f19603f3d011682016040523d82523d6000602084013e612009565b606091505b509150915061201a87838387612026565b92505050949350505050565b60608315612088576000835103612080576120408561209b565b61207f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016120769061348a565b60405180910390fd5b5b829050612093565b61209283836120be565b5b949350505050565b6000808273ffffffffffffffffffffffffffffffffffffffff163b119050919050565b6000825111156120d15781518083602001fd5b806040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161210591906128f4565b60405180910390fd5b6040518060800160405280600081526020016000815260200160008152602001600081525090565b6040518060c00160405280600067ffffffffffffffff168152602001600067ffffffffffffffff168152602001600067ffffffffffffffff16815260200160008152602001600073ffffffffffffffffffffffffffffffffffffffff168152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b604051806040016040528060608152602001600081525090565b6000604051905090565b600080fd5b600080fd5b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b612219816121e4565b811461222457600080fd5b50565b60008135905061223681612210565b92915050565b600060208284031215612252576122516121da565b5b600061226084828501612227565b91505092915050565b60008115159050919050565b61227e81612269565b82525050565b60006020820190506122996000830184612275565b92915050565b6000819050919050565b6122b28161229f565b81146122bd57600080fd5b50565b6000813590506122cf816122a9565b92915050565b6000602082840312156122eb576122ea6121da565b5b60006122f9848285016122c0565b91505092915050565b61230b8161229f565b82525050565b60006020820190506123266000830184612302565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006123578261232c565b9050919050565b6123678161234c565b82525050565b6000602082019050612382600083018461235e565b92915050565b6123918161229f565b82525050565b6080820160008201516123ad6000850182612388565b5060208201516123c06020850182612388565b5060408201516123d36040850182612388565b5060608201516123e66060850182612388565b50505050565b60006080820190506124016000830184612397565b92915050565b6124108161234c565b811461241b57600080fd5b50565b60008135905061242d81612407565b92915050565b6000806040838503121561244a576124496121da565b5b60006124588582860161241e565b9250506020612469858286016122c0565b9150509250929050565b600067ffffffffffffffff82169050919050565b61249081612473565b82525050565b61249f8161234c565b82525050565b60c0820160008201516124bb6000850182612487565b5060208201516124ce6020850182612487565b5060408201516124e16040850182612487565b5060608201516124f46060850182612388565b5060808201516125076080850182612496565b5060a082015161251a60a0850182612496565b50505050565b600060c08201905061253560008301846124a5565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126125605761255f61253b565b5b8235905067ffffffffffffffff81111561257d5761257c612540565b5b60208301915083600182028301111561259957612598612545565b5b9250929050565b60008060008060008060a087890312156125bd576125bc6121da565b5b60006125cb89828a0161241e565b96505060206125dc89828a016122c0565b95505060406125ed89828a016122c0565b94505060606125fe89828a016122c0565b935050608087013567ffffffffffffffff81111561261f5761261e6121df565b5b61262b89828a0161254a565b92509250509295509295509295565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b600381106126a6576126a5612666565b5b50565b60008190506126b782612695565b919050565b60006126c7826126a9565b9050919050565b6126d7816126bc565b82525050565b600081519050919050565b600082825260208201905092915050565b60005b838110156127175780820151818401526020810190506126fc565b60008484015250505050565b6000601f19601f8301169050919050565b600061273f826126dd565b61274981856126e8565b93506127598185602086016126f9565b61276281612723565b840191505092915050565b600060408301600083015161278560008601826126ce565b506020830151848203602086015261279d8282612734565b9150508091505092915050565b60006127b6838361276d565b905092915050565b6000602082019050919050565b60006127d68261263a565b6127e08185612645565b9350836020820285016127f285612656565b8060005b8581101561282e578484038952815161280f85826127aa565b945061281a836127be565b925060208a019950506001810190506127f6565b50829750879550505050505092915050565b6000604083016000830151848203600086015261285d82826127cb565b91505060208301516128726020860182612388565b508091505092915050565b600060208201905081810360008301526128978184612840565b905092915050565b600081519050919050565b600082825260208201905092915050565b60006128c68261289f565b6128d081856128aa565b93506128e08185602086016126f9565b6128e981612723565b840191505092915050565b6000602082019050818103600083015261290e81846128bb565b905092915050565b60008060006060848603121561292f5761292e6121da565b5b600061293d8682870161241e565b935050602061294e868287016122c0565b925050604061295f8682870161241e565b9150509250925092565b60006020828403121561297f5761297e6121da565b5b600061298d8482850161241e565b91505092915050565b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6129d382612723565b810181811067ffffffffffffffff821117156129f2576129f161299b565b5b80604052505050565b6000612a056121d0565b9050612a1182826129ca565b919050565b612a1f81612473565b8114612a2a57600080fd5b50565b600081359050612a3c81612a16565b92915050565b600060c08284031215612a5857612a57612996565b5b612a6260c06129fb565b90506000612a7284828501612a2d565b6000830152506020612a8684828501612a2d565b6020830152506040612a9a84828501612a2d565b6040830152506060612aae848285016122c0565b6060830152506080612ac28482850161241e565b60808301525060a0612ad68482850161241e565b60a08301525092915050565b60008060e08385031215612af957612af86121da565b5b6000612b07858286016122c0565b9250506020612b1885828601612a42565b9150509250929050565b60008060408385031215612b3957612b386121da565b5b6000612b47858286016122c0565b9250506020612b58858286016122c0565b9150509250929050565b60008083601f840112612b7857612b7761253b565b5b8235905067ffffffffffffffff811115612b9557612b94612540565b5b602083019150836001820283011115612bb157612bb0612545565b5b9250929050565b60008060008060008060008060006101008a8c031215612bdb57612bda6121da565b5b6000612be98c828d0161241e565b9950506020612bfa8c828d016122c0565b9850506040612c0b8c828d0161241e565b9750506060612c1c8c828d016122c0565b9650506080612c2d8c828d016122c0565b95505060a0612c3e8c828d0161241e565b94505060c0612c4f8c828d0161241e565b93505060e08a013567ffffffffffffffff811115612c7057612c6f6121df565b5b612c7c8c828d01612b62565b92509250509295985092959850929598565b60008160001c9050919050565b600067ffffffffffffffff82169050919050565b6000612cc2612cbd83612c8e565b612c9b565b9050919050565b60008160401c9050919050565b6000612ce9612ce483612cc9565b612c9b565b9050919050565b60008160801c9050919050565b6000612d10612d0b83612cf0565b612c9b565b9050919050565b6000819050919050565b6000612d34612d2f83612c8e565b612d17565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000612d6e612d6983612c8e565b612d3b565b9050919050565b60c082016000808301549050612d8a81612caf565b612d976000860182612487565b50612da181612cd6565b612dae6020860182612487565b50612db881612cfd565b612dc56040860182612487565b5060018301549050612dd681612d21565b612de36060860182612388565b5060028301549050612df481612d5b565b612e016080860182612496565b5060038301549050612e1281612d5b565b612e1f60a0860182612496565b5050505050565b600060c082019050612e3b6000830184612d75565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000612e7b8261229f565b9150612e868361229f565b9250828202612e948161229f565b91508282048414831517612eab57612eaa612e41565b5b5092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b6000612eec8261229f565b9150612ef78361229f565b925082612f0757612f06612eb2565b5b828204905092915050565b6000612f1d8261229f565b9150612f288361229f565b9250828201905080821115612f4057612f3f612e41565b5b92915050565b6000612f518261229f565b9150612f5c8361229f565b9250828203905081811115612f7457612f73612e41565b5b92915050565b600081519050612f8981612407565b92915050565b600060208284031215612fa557612fa46121da565b5b6000612fb384828501612f7a565b91505092915050565b600082825260208201905092915050565b50565b6000612fdd600083612fbc565b9150612fe882612fcd565b600082019050919050565b6000608082019050613008600083018661235e565b6130156020830185612302565b6130226040830184612302565b818103606083015261303381612fd0565b9050949350505050565b82818337600083830152505050565b600061305883856128aa565b935061306583858461303d565b61306e83612723565b840190509392505050565b600060408201905061308e6000830186612302565b81810360208301526130a181848661304c565b9050949350505050565b7f5265656e7472616e637947756172643a207265656e7472616e742063616c6c00600082015250565b60006130e1601f836128aa565b91506130ec826130ab565b602082019050919050565b60006020820190508181036000830152613110816130d4565b9050919050565b600060608201905061312c600083018661235e565b6131396020830185612302565b6131466040830184612302565b949350505050565b60008151905061315d816122a9565b92915050565b600060208284031215613179576131786121da565b5b60006131878482850161314e565b91505092915050565b6000610100820190506131a6600083018b61235e565b6131b3602083018a61235e565b6131c0604083018961235e565b6131cd6060830188612302565b6131da6080830187612302565b6131e760a0830186612302565b6131f460c0830185612302565b61320160e0830184612302565b9998505050505050505050565b6000604082019050613223600083018561235e565b6132306020830184612302565b9392505050565b600060608201905061324c600083018661235e565b613259602083018561235e565b6132666040830184612302565b949350505050565b61327781612269565b811461328257600080fd5b50565b6000815190506132948161326e565b92915050565b6000602082840312156132b0576132af6121da565b5b60006132be84828501613285565b91505092915050565b7f5361666545524332303a204552433230206f7065726174696f6e20646964206e60008201527f6f74207375636365656400000000000000000000000000000000000000000000602082015250565b6000613323602a836128aa565b915061332e826132c7565b604082019050919050565b6000602082019050818103600083015261335281613316565b9050919050565b7f416464726573733a20696e73756666696369656e742062616c616e636520666f60008201527f722063616c6c0000000000000000000000000000000000000000000000000000602082015250565b60006133b56026836128aa565b91506133c082613359565b604082019050919050565b600060208201905081810360008301526133e4816133a8565b9050919050565b600081905092915050565b6000613401826126dd565b61340b81856133eb565b935061341b8185602086016126f9565b80840191505092915050565b600061343382846133f6565b915081905092915050565b7f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000600082015250565b6000613474601d836128aa565b915061347f8261343e565b602082019050919050565b600060208201905081810360008301526134a381613467565b905091905056fe68747470733a2f2f6769746875622e636f6d2f6f75727a6f72612f7a6f72612d70726f746f636f6c2fa2646970667358221220df2d8476e04ee3234e45530604e862841e20639348e70c94f4687b77a27195ab64736f6c63430008110033", + "salt": "0x00000000000000000000000000000000000000003cebb365bb74a80061bce7d6", + "expectedAddress": "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + } \ No newline at end of file diff --git a/packages/creator-subgraph/abis/ERC20Minter.json b/packages/creator-subgraph/abis/ERC20Minter.json new file mode 100644 index 000000000..997d43071 --- /dev/null +++ b/packages/creator-subgraph/abis/ERC20Minter.json @@ -0,0 +1,725 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_zoraRewardAddress", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "computePaidMintRewards", + "inputs": [ + { + "name": "totalReward", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IERC20Minter.RewardsSettings", + "components": [ + { + "name": "createReferralReward", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "mintReferralReward", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zoraReward", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "firstMinterReward", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "computeReward", + "inputs": [ + { + "name": "totalReward", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "rewardPct", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "computeTotalReward", + "inputs": [ + { + "name": "totalValue", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "contractName", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "contractURI", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "contractVersion", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "getCreateReferral", + "inputs": [ + { + "name": "tokenContract", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "createReferral", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getFirstMinter", + "inputs": [ + { + "name": "tokenContract", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "firstMinter", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMintedPerWallet", + "inputs": [ + { + "name": "tokenContract", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "wallet", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "mintTo", + "type": "address", + "internalType": "address" + }, + { + "name": "quantity", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "totalValue", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "currency", + "type": "address", + "internalType": "address" + }, + { + "name": "mintReferral", + "type": "address", + "internalType": "address" + }, + { + "name": "comment", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "requestMint", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct ICreatorCommands.CommandSet", + "components": [ + { + "name": "commands", + "type": "tuple[]", + "internalType": "struct ICreatorCommands.Command[]", + "components": [ + { + "name": "method", + "type": "uint8", + "internalType": "enum ICreatorCommands.CreatorActions" + }, + { + "name": "args", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "at", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "resetSale", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "sale", + "inputs": [ + { + "name": "tokenContract", + "type": "address", + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IERC20Minter.SalesConfig", + "components": [ + { + "name": "saleStart", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "saleEnd", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "maxTokensPerAddress", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "pricePerToken", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fundsRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "currency", + "type": "address", + "internalType": "address" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setSale", + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "salesConfig", + "type": "tuple", + "internalType": "struct IERC20Minter.SalesConfig", + "components": [ + { + "name": "saleStart", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "saleEnd", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "maxTokensPerAddress", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "pricePerToken", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fundsRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "currency", + "type": "address", + "internalType": "address" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "totalRewardPct", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "zoraRewardAddress", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "ERC20MinterInitialized", + "inputs": [ + { + "name": "rewardPercentage", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "ERC20RewardsDeposit", + "inputs": [ + { + "name": "createReferral", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "mintReferral", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "firstMinter", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "zora", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "collection", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "currency", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "createReferralReward", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "mintReferralReward", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "firstMinterReward", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "zoraReward", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MintComment", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenContract", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "quantity", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "comment", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SaleSet", + "inputs": [ + { + "name": "mediaContract", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "salesConfig", + "type": "tuple", + "indexed": false, + "internalType": "struct IERC20Minter.SalesConfig", + "components": [ + { + "name": "saleStart", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "saleEnd", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "maxTokensPerAddress", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "pricePerToken", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "fundsRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "currency", + "type": "address", + "internalType": "address" + } + ] + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AddressZero", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidCurrency", + "inputs": [] + }, + { + "type": "error", + "name": "PricePerTokenTooLow", + "inputs": [] + }, + { + "type": "error", + "name": "RequestMintInvalidUseMint", + "inputs": [] + }, + { + "type": "error", + "name": "SaleEnded", + "inputs": [] + }, + { + "type": "error", + "name": "SaleHasNotStarted", + "inputs": [] + }, + { + "type": "error", + "name": "UserExceedsMintLimit", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requestedAmount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "WrongValueSent", + "inputs": [] + } + ] + \ No newline at end of file diff --git a/packages/creator-subgraph/config/arbitrum-one.yaml b/packages/creator-subgraph/config/arbitrum-one.yaml index 12956a8bc..a48990770 100644 --- a/packages/creator-subgraph/config/arbitrum-one.yaml +++ b/packages/creator-subgraph/config/arbitrum-one.yaml @@ -11,3 +11,7 @@ protocolRewards: - address: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B" startBlock: "169101021" version: "1" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "194476347" + version: "1" \ No newline at end of file diff --git a/packages/creator-subgraph/config/arbitrum-sepolia.yaml b/packages/creator-subgraph/config/arbitrum-sepolia.yaml index 7b1ee7aa0..9bb62ca0a 100644 --- a/packages/creator-subgraph/config/arbitrum-sepolia.yaml +++ b/packages/creator-subgraph/config/arbitrum-sepolia.yaml @@ -11,3 +11,7 @@ protocolRewards: - address: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B" startBlock: "10701645" version: "1" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "27217036" + version: "1" \ No newline at end of file diff --git a/packages/creator-subgraph/config/base-mainnet.yaml b/packages/creator-subgraph/config/base-mainnet.yaml index 7cadf73bb..46c20c97a 100644 --- a/packages/creator-subgraph/config/base-mainnet.yaml +++ b/packages/creator-subgraph/config/base-mainnet.yaml @@ -18,3 +18,7 @@ protocolRewards: - address: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B" startBlock: "2336418" version: "2" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "12341612" + version: "1" \ No newline at end of file diff --git a/packages/creator-subgraph/config/blast.yaml b/packages/creator-subgraph/config/blast.yaml index da56da3a0..c9f1b79a6 100644 --- a/packages/creator-subgraph/config/blast.yaml +++ b/packages/creator-subgraph/config/blast.yaml @@ -12,3 +12,7 @@ protocolRewards: - address: "0x7777777A456fF23D9b6851184472c08FBDa73e32" startBlock: "2123378" version: "1" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "1331788" + version: "1" diff --git a/packages/creator-subgraph/config/mainnet.yaml b/packages/creator-subgraph/config/mainnet.yaml index 573f2e359..7e30e978f 100644 --- a/packages/creator-subgraph/config/mainnet.yaml +++ b/packages/creator-subgraph/config/mainnet.yaml @@ -26,3 +26,7 @@ protocolRewards: - address: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B" startBlock: "17867249" version: "2" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "19520247" + version: "1" diff --git a/packages/creator-subgraph/config/optimism.yaml b/packages/creator-subgraph/config/optimism.yaml index 09b715a24..fe1c0f87b 100644 --- a/packages/creator-subgraph/config/optimism.yaml +++ b/packages/creator-subgraph/config/optimism.yaml @@ -18,3 +18,7 @@ protocolRewards: - address: "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B" startBlock: "107931668" version: "2" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "117937046" + version: "1" \ No newline at end of file diff --git a/packages/creator-subgraph/config/sepolia.yaml b/packages/creator-subgraph/config/sepolia.yaml index d6382e35a..fb369ec2b 100644 --- a/packages/creator-subgraph/config/sepolia.yaml +++ b/packages/creator-subgraph/config/sepolia.yaml @@ -8,3 +8,7 @@ factories721: - address: "0x87cfd516c5ea86e50b950678CA970a8a28de27ac" startBlock: "3354415" version: "1" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "5566250" + version: "1" \ No newline at end of file diff --git a/packages/creator-subgraph/config/zora-mainnet.yaml b/packages/creator-subgraph/config/zora-mainnet.yaml index c6d2b34ff..72223f759 100644 --- a/packages/creator-subgraph/config/zora-mainnet.yaml +++ b/packages/creator-subgraph/config/zora-mainnet.yaml @@ -26,3 +26,7 @@ reserveAuctionV3: - address: "0xA06262157905913f855573f53AD48DE2D4ba1F4A" startBlock: "6686902" version: "1" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "12389159" + version: "1" diff --git a/packages/creator-subgraph/config/zora-sepolia.yaml b/packages/creator-subgraph/config/zora-sepolia.yaml index 10a54af5e..d15edc5af 100644 --- a/packages/creator-subgraph/config/zora-sepolia.yaml +++ b/packages/creator-subgraph/config/zora-sepolia.yaml @@ -23,4 +23,8 @@ reserveAuctionV3: zoraMints: - address: "0x77777773dE7607C8d2eF571ba03ab22a7df64CEA" startBlock: "4637159" - version: "1" \ No newline at end of file + version: "1" +erc20Minter: + - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" + startBlock: "6695930" + version: "1" diff --git a/packages/creator-subgraph/schema.graphql b/packages/creator-subgraph/schema.graphql index fb9381b3b..f7a609bde 100644 --- a/packages/creator-subgraph/schema.graphql +++ b/packages/creator-subgraph/schema.graphql @@ -412,6 +412,7 @@ type SalesStrategyConfig @entity { presale: SalesConfigMerkleMinterStrategy fixedPrice: SalesConfigFixedPriceSaleStrategy redeemMinter: SalesConfigRedeemMinterStrategy + erc20Minter: SalesConfigERC20Minter type: String! } @@ -603,3 +604,49 @@ type MintAccountBalance @entity { account: Bytes! mintToken: MintToken! } + +### ERC20 Minter + +type SalesConfigERC20Minter @entity { + # Begin – default data block + id: ID! + txn: TransactionInfo! + block: BigInt! + address: Bytes! + timestamp: BigInt! + # End – default data block + + configAddress: Bytes! + + tokenId: BigInt! + contract: ZoraCreateContract! + saleStart: BigInt! + saleEnd: BigInt! + maxTokensPerAddress: BigInt! + pricePerToken: BigInt! + currency: Bytes! + + fundsRecipient: Bytes +} + +type ERC20RewardsDeposit @entity @immutable { + # Begin – default data block + id: ID! + txn: TransactionInfo! + block: BigInt! + address: Bytes! + timestamp: BigInt! + # End – default data block + + collection: Bytes! + mintReferral: Bytes! + mintReferralReward: BigInt! + createReferral: Bytes! + createReferralReward: BigInt! + zora: Bytes! + zoraReward: BigInt! + currency: Bytes! + tokenId: BigInt! + firstMinter: Bytes! + firstMinterReward: BigInt! +} diff --git a/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts b/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts new file mode 100644 index 000000000..4a4bb1027 --- /dev/null +++ b/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts @@ -0,0 +1,105 @@ +import { + MintComment as ERC20MintComment, + SaleSet, + ERC20RewardsDeposit as ERC20RewardsDepositEvent, +} from "../../generated/ERC20Minter/ERC20Minter"; +import { BigInt } from "@graphprotocol/graph-ts"; +import { getSalesConfigKey } from "../common/getSalesConfigKey"; +import { getTokenId } from "../common/getTokenId"; +import { makeTransaction } from "../common/makeTransaction"; +import { SALE_CONFIG_ERC_20_MINTER } from "../constants/salesConfigTypes"; +import { getContractId } from "../common/getContractId"; +import { + SalesConfigERC20Minter, + SalesStrategyConfig, + MintComment, + ERC20RewardsDeposit, +} from "../../generated/schema"; +import { getMintCommentId } from "../common/getMintCommentId"; + +export function handleERC20MinterSaleSet(event: SaleSet): void { + const id = getSalesConfigKey( + event.address, + event.params.mediaContract, + event.params.tokenId, + ); + let sale = new SalesConfigERC20Minter(id); + sale.configAddress = event.address; + sale.saleStart = event.params.salesConfig.saleStart; + sale.contract = getContractId(event.params.mediaContract); + sale.fundsRecipient = event.params.salesConfig.fundsRecipient; + sale.pricePerToken = event.params.salesConfig.pricePerToken; + sale.saleEnd = event.params.salesConfig.saleEnd; + sale.maxTokensPerAddress = event.params.salesConfig.maxTokensPerAddress; + sale.currency = event.params.salesConfig.currency; + sale.tokenId = event.params.tokenId; + const txn = makeTransaction(event); + sale.txn = txn; + sale.block = event.block.number; + sale.timestamp = event.block.timestamp; + sale.address = event.address; + + sale.save(); + + const saleJoin = new SalesStrategyConfig(id); + if (event.params.tokenId.equals(BigInt.zero())) { + saleJoin.contract = getContractId(event.params.mediaContract); + } else { + saleJoin.tokenAndContract = getTokenId( + event.params.mediaContract, + event.params.tokenId, + ); + } + saleJoin.erc20Minter = id; + saleJoin.type = SALE_CONFIG_ERC_20_MINTER; + saleJoin.txn = txn; + saleJoin.block = event.block.number; + saleJoin.timestamp = event.block.timestamp; + saleJoin.address = event.address; + saleJoin.save(); +} + +export function handleMintComment(event: ERC20MintComment): void { + const mintComment = new MintComment(getMintCommentId(event)); + const tokenAndContract = getTokenId( + event.params.tokenContract, + event.params.tokenId, + ); + mintComment.tokenAndContract = tokenAndContract; + mintComment.sender = event.params.sender; + mintComment.comment = event.params.comment; + mintComment.mintQuantity = event.params.quantity; + mintComment.tokenId = event.params.tokenId; + + mintComment.txn = makeTransaction(event); + mintComment.block = event.block.number; + mintComment.timestamp = event.block.timestamp; + mintComment.address = event.address; + + mintComment.save(); +} + +export function handleERC20RewardsDeposit( + event: ERC20RewardsDepositEvent, +): void { + const rewardsDeposit = new ERC20RewardsDeposit( + `${event.transaction.hash.toHex()}-${event.transactionLogIndex}`, + ); + rewardsDeposit.address = event.address; + rewardsDeposit.block = event.block.number; + rewardsDeposit.timestamp = event.block.timestamp; + rewardsDeposit.txn = makeTransaction(event); + rewardsDeposit.collection = event.params.collection; + rewardsDeposit.mintReferral = event.params.mintReferral; + rewardsDeposit.mintReferralReward = event.params.mintReferralReward; + rewardsDeposit.createReferral = event.params.createReferral; + rewardsDeposit.createReferralReward = event.params.createReferralReward; + rewardsDeposit.zora = event.params.zora; + rewardsDeposit.zoraReward = event.params.zoraReward; + rewardsDeposit.currency = event.params.currency; + rewardsDeposit.tokenId = event.params.tokenId; + rewardsDeposit.firstMinter = event.params.firstMinter; + rewardsDeposit.firstMinterReward = event.params.firstMinterReward; + + rewardsDeposit.save(); +} diff --git a/packages/creator-subgraph/src/constants/salesConfigTypes.ts b/packages/creator-subgraph/src/constants/salesConfigTypes.ts index 0a28353d5..4e4bfa124 100644 --- a/packages/creator-subgraph/src/constants/salesConfigTypes.ts +++ b/packages/creator-subgraph/src/constants/salesConfigTypes.ts @@ -1,3 +1,4 @@ -export const SALE_CONFIG_PRESALE = 'PRESALE' -export const SALE_CONFIG_FIXED_PRICE = 'FIXED_PRICE' -export const SALE_CONFIG_REDEEM_STRATEGY = 'REDEEM_STRATEGY' \ No newline at end of file +export const SALE_CONFIG_PRESALE = "PRESALE"; +export const SALE_CONFIG_FIXED_PRICE = "FIXED_PRICE"; +export const SALE_CONFIG_REDEEM_STRATEGY = "REDEEM_STRATEGY"; +export const SALE_CONFIG_ERC_20_MINTER = "ERC_20_MINTER"; diff --git a/packages/creator-subgraph/subgraph.template.yaml b/packages/creator-subgraph/subgraph.template.yaml index f1fc2be26..b3827bad0 100644 --- a/packages/creator-subgraph/subgraph.template.yaml +++ b/packages/creator-subgraph/subgraph.template.yaml @@ -174,6 +174,34 @@ dataSources: - event: TransferBatch(indexed address,indexed address,indexed address,uint256[],uint256[]) handler: handleTransferBatch {{/zoraMints}} +{{#erc20Minter}} + - name: ERC20Minter + kind: ethereum/contract + network: {{network}} + source: + abi: ERC20Minter + address: "{{address}}" + startBlock: {{startBlock}} + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + entities: + - SalesConfigERC20Minter + - MintComment + - ERC20RewardsDeposit + language: wasm/assemblyscript + file: ./src/ERC1155Mappings/ERC20MinterMappings.ts + abis: + - name: ERC20Minter + file: ./abis/ERC20Minter.json + eventHandlers: + - event: SaleSet(indexed address,indexed uint256,(uint64,uint64,uint64,uint256,address,address)) + handler: handleERC20MinterSaleSet + - event: MintComment(indexed address,indexed address,indexed uint256,uint256,string) + handler: handleMintComment + - event: ERC20RewardsDeposit(indexed address,indexed address,indexed address,address,address,address,uint256,uint256,uint256,uint256,uint256) + handler: handleERC20RewardsDeposit +{{/erc20Minter}} templates: - name: MetadataInfo kind: file/ipfs diff --git a/packages/shared-contracts/src/entities/Premint.sol b/packages/shared-contracts/src/entities/Premint.sol index 695b8a821..0b21033f4 100644 --- a/packages/shared-contracts/src/entities/Premint.sol +++ b/packages/shared-contracts/src/entities/Premint.sol @@ -86,6 +86,43 @@ struct TokenCreationConfigV2 { address createReferral; } +struct Erc20PremintConfigV1 { + // The config for the token to be created + Erc20TokenCreationConfigV1 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 Erc20TokenCreationConfigV1 { + // Metadata URI for the created token + string tokenURI; + // Max supply of the created token + uint256 maxSupply; + // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + uint32 royaltyBPS; + // The address that the will receive rewards/funds/royalties. + address payoutRecipient; + // The address that referred the creation of the token. + address createReferral; + // The address of the ERC20 minter module. + address erc20Minter; + // The start time of the mint, 0 for immediate. + uint64 mintStart; + // The duration of the mint, starting from the first mint of this token. 0 for infinite + uint64 mintDuration; + // Max tokens that can be minted for an address, 0 if unlimited + uint64 maxTokensPerAddress; + // The ERC20 currency address + address currency; + // Price per token in ERC20 currency + uint256 pricePerToken; +} + struct MintArguments { address mintRecipient; string mintComment; diff --git a/packages/shared-contracts/src/interfaces/errors/IZoraCreator1155Errors.sol b/packages/shared-contracts/src/interfaces/errors/IZoraCreator1155Errors.sol index 3b0227244..8feb5af68 100644 --- a/packages/shared-contracts/src/interfaces/errors/IZoraCreator1155Errors.sol +++ b/packages/shared-contracts/src/interfaces/errors/IZoraCreator1155Errors.sol @@ -52,4 +52,6 @@ interface IZoraCreator1155Errors is ICreatorRoyaltyErrors, ILimitedMintPerAddres error FirstMinterAddressZero(); error ERC1155_MINT_TO_ZERO_ADDRESS(); + + error InvalidPremintVersion(); } diff --git a/packages/shared-contracts/src/premint/PremintEncoding.sol b/packages/shared-contracts/src/premint/PremintEncoding.sol index 76e7e2e78..f31e67be0 100644 --- a/packages/shared-contracts/src/premint/PremintEncoding.sol +++ b/packages/shared-contracts/src/premint/PremintEncoding.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import {PremintConfig, PremintConfigV2} from "../entities/Premint.sol"; +import {PremintConfig, PremintConfigV2, Erc20PremintConfigV1} from "../entities/Premint.sol"; import {IMinter1155} from "../interfaces/IMinter1155.sol"; struct EncodedPremintConfig { bytes premintConfig; bytes32 premintConfigVersion; uint32 uid; - address fixedPriceMinter; + address minter; } library PremintEncoding { @@ -16,6 +16,8 @@ library PremintEncoding { bytes32 internal constant HASHED_VERSION_1 = keccak256(bytes(VERSION_1)); string internal constant VERSION_2 = "2"; bytes32 internal constant HASHED_VERSION_2 = keccak256(bytes(VERSION_2)); + string internal constant ERC20_VERSION_1 = "ERC20_1"; + bytes32 internal constant HASHED_ERC20_VERSION_1 = keccak256(bytes(ERC20_VERSION_1)); function encodePremintV1(PremintConfig memory premintConfig) internal pure returns (EncodedPremintConfig memory) { return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_1, premintConfig.uid, premintConfig.tokenConfig.fixedPriceMinter); @@ -24,4 +26,8 @@ library PremintEncoding { function encodePremintV2(PremintConfigV2 memory premintConfig) internal pure returns (EncodedPremintConfig memory) { return EncodedPremintConfig(abi.encode(premintConfig), HASHED_VERSION_2, premintConfig.uid, premintConfig.tokenConfig.fixedPriceMinter); } + + function encodePremintErc20V1(Erc20PremintConfigV1 memory premintConfig) internal pure returns (EncodedPremintConfig memory) { + return EncodedPremintConfig(abi.encode(premintConfig), HASHED_ERC20_VERSION_1, premintConfig.uid, premintConfig.tokenConfig.erc20Minter); + } }