From cd6c6361516ab9a40821bfd56cc8eba36b0f3851 Mon Sep 17 00:00:00 2001 From: Isabella Smallcombe Date: Wed, 15 May 2024 13:25:15 -0400 Subject: [PATCH] ERC20Minter with ETH Fee (#372) * feat: wip of erc20 minter with new fees * fix: events * fix: function type * feat: add _ to internal functions * fix: checks * fix: tests * feat: clean up internal functions * feat: add ERC20MinterConfig struct * feat: add init check * Updates for ownership handling (#377) * fix: tests * feat: add changeset * feat: remove ERC20MinterInitialized event * feat: add detailed changeset * fix: event test * fix: remove eth reward not equal to zero * feat: add zora sepolia erc20 minter v2 deployment address * fix: changelog * fix: vm.assume * feat: add erc20 minter to abi script * fix: file import * fix: skip testPremintERC20 --------- Co-authored-by: Iain Nash --- .changeset/funny-grapes-impress.md | 19 ++ .../src/interfaces/IERC20Minter.sol | 47 +++-- .../src/minters/erc20/ERC20Minter.sol | 113 ++++++---- .../src/minters/erc20/ERC20MinterRewards.sol | 1 - .../test/minters/erc20/ERC20Minter.t.sol | 199 ++++++++++++++---- .../test/premint/PremintERC20.t.sol | 10 +- .../creator-subgraph/config/zora-sepolia.yaml | 3 + .../creator-subgraph/scripts/extract-abis.mjs | 3 + .../ERC1155Mappings/ERC20MinterMappings.ts | 2 +- .../creator-subgraph/subgraph.template.yaml | 2 +- 10 files changed, 302 insertions(+), 97 deletions(-) create mode 100644 .changeset/funny-grapes-impress.md diff --git a/.changeset/funny-grapes-impress.md b/.changeset/funny-grapes-impress.md new file mode 100644 index 00000000..c626ea57 --- /dev/null +++ b/.changeset/funny-grapes-impress.md @@ -0,0 +1,19 @@ +--- +"@zoralabs/zora-1155-contracts": patch +--- + +ERC20 Minter V2 Changes: +* Adds a flat ETH fee that goes to Zora (currently this fee is 0.000111 ETH but the contract owner can change this fee at any time) +* Reward recipients will still receive ERC20 rewards however this percentage can now be changed at any time by the contract owner +* Adds an `ERC20MinterConfig` struct which contains `zoraRewardRecipientAddress`, `rewardRecipientPercentage`, and `ethReward` +* Zora Reward Recipient Address can now be changed at any time by the contract owner as well +* `mint` function is now payable +* New functions: + * `function ethRewardAmount() external view returns (uint256)` + * `function setERC20MinterConfig(ERC20MinterConfig memory config) external` + * `function getERC20MinterConfig() external view returns (ERC20MinterConfig memory)` +* New events: + * `event ERC20MinterConfigSet(ERC20MinterConfig config)` +* Removed events: + * `event ZoraRewardsRecipientSet(address indexed prevRecipient, address indexed newRecipient)` + * `event ERC20MinterInitialized(uint256 rewardPercentage)` diff --git a/packages/1155-contracts/src/interfaces/IERC20Minter.sol b/packages/1155-contracts/src/interfaces/IERC20Minter.sol index d4a62456..3899aeae 100644 --- a/packages/1155-contracts/src/interfaces/IERC20Minter.sol +++ b/packages/1155-contracts/src/interfaces/IERC20Minter.sol @@ -28,6 +28,15 @@ interface IERC20Minter { address currency; } + struct ERC20MinterConfig { + /// @notice The address of the Zora rewards recipient + address zoraRewardRecipientAddress; + /// @notice The reward recipient percentage + uint256 rewardRecipientPercentage; + /// @notice The ETH reward amount + uint256 ethReward; + } + /// @notice Rewards Deposit Event /// @param createReferral Creator referral address /// @param mintReferral Mint referral address @@ -54,10 +63,6 @@ interface IERC20Minter { 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 @@ -72,10 +77,9 @@ interface IERC20Minter { /// @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 ERC20MinterConfigSet Event + /// @param config The ERC20MinterConfig + event ERC20MinterConfigSet(ERC20MinterConfig config); /// @notice Cannot set address to zero error AddressZero(); @@ -101,11 +105,11 @@ interface IERC20Minter { /// @notice ERC20 transfer slippage error ERC20TransferSlippage(); - /// @notice ERC20Minter is already initialized - error AlreadyInitialized(); + /// @notice Failed to send ETH reward + error FailedToSendEthReward(); - /// @notice Only the Zora rewards recipient can call this function - error OnlyZoraRewardsRecipient(); + /// @notice Invalid value + error InvalidValue(); /// @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 @@ -125,11 +129,28 @@ interface IERC20Minter { address currency, address mintReferral, string calldata comment - ) external; + ) external payable; /// @notice Sets the sale config for a given token + /// @param tokenId The ID of the token to set the sale config for + /// @param salesConfig The sale config to set function setSale(uint256 tokenId, SalesConfig memory salesConfig) external; /// @notice Returns the sale config for a given token + /// @param tokenContract The TokenContract address + /// @param tokenId The ID of the token to get the sale config for function sale(address tokenContract, uint256 tokenId) external view returns (SalesConfig memory); + + /// @notice Returns the reward recipient percentage + function totalRewardPct() external view returns (uint256); + + /// @notice Returns the ETH reward amount + function ethRewardAmount() external view returns (uint256); + + /// @notice Sets the ERC20MinterConfig + /// @param config The ERC20MinterConfig to set + function setERC20MinterConfig(ERC20MinterConfig memory config) external; + + /// @notice Gets the ERC20MinterConfig + function getERC20MinterConfig() external view returns (ERC20MinterConfig memory); } diff --git a/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol b/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol index b22c2a44..2105976f 100644 --- a/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol +++ b/packages/1155-contracts/src/minters/erc20/ERC20Minter.sol @@ -11,6 +11,9 @@ import {SaleStrategy} from "../../minters/SaleStrategy.sol"; import {ICreatorCommands} from "../../interfaces/ICreatorCommands.sol"; import {ERC20MinterRewards} from "./ERC20MinterRewards.sol"; import {IZora1155} from "./IZora1155.sol"; +import {TransferHelperUtils} from "../../utils/TransferHelperUtils.sol"; +import {Initializable} from "@zoralabs/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {Ownable2StepUpgradeable} from "../../utils/ownable/Ownable2StepUpgradeable.sol"; /* @@ -41,11 +44,11 @@ import {IZora1155} from "./IZora1155.sol"; /// @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 { +contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMintPerAddress, ERC20MinterRewards, Initializable, Ownable2StepUpgradeable { using SafeERC20 for IERC20; - /// @notice The address of the Zora rewards recipient - address public zoraRewardRecipientAddress; + /// @notice The ERC20 minter configuration + ERC20MinterConfig public minterConfig; /// @notice The ERC20 sale configuration for a given 1155 token /// @dev 1155 token address => 1155 token id => SalesConfig @@ -53,24 +56,17 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint /// @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); + function initialize(address _zoraRewardRecipientAddress, address owner, uint256 _rewardPct, uint256 _ethReward) external initializer { + __Ownable_init(owner); + _setERC20MinterConfig( + ERC20MinterConfig({zoraRewardRecipientAddress: _zoraRewardRecipientAddress, rewardRecipientPercentage: _rewardPct, ethReward: _ethReward}) + ); } /// @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; + function computeTotalReward(uint256 totalValue) public view returns (uint256) { + return (totalValue * minterConfig.rewardRecipientPercentage) / BPS_TO_PERCENT_2_DECIMAL_PERCISION; } /// @notice Computes the rewards value given an amount and a reward percentage @@ -106,7 +102,7 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint } catch {} if (createReferral == address(0)) { - createReferral = zoraRewardRecipientAddress; + createReferral = minterConfig.zoraRewardRecipientAddress; } } @@ -121,10 +117,15 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint firstMinter = IZora1155(tokenContract).getCreatorRewardRecipient(tokenId); } } catch { - firstMinter = zoraRewardRecipientAddress; + firstMinter = minterConfig.zoraRewardRecipientAddress; } } + /// @notice Gets the ERC20MinterConfig + function getERC20MinterConfig() external view returns (ERC20MinterConfig memory) { + return minterConfig; + } + /// @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 @@ -151,19 +152,19 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint address firstMinter = getFirstMinter(tokenAddress, tokenId); if (mintReferral == address(0)) { - mintReferral = zoraRewardRecipientAddress; + mintReferral = minterConfig.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); + IERC20(currency).safeTransfer(minterConfig.zoraRewardRecipientAddress, settings.zoraReward); emit ERC20RewardsDeposit( createReferral, mintReferral, firstMinter, - zoraRewardRecipientAddress, + minterConfig.zoraRewardRecipientAddress, tokenAddress, currency, tokenId, @@ -174,6 +175,14 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint ); } + /// @notice Distributes the ETH rewards to the Zora rewards recipient + /// @param ethSent The amount of ETH to distribute + function _distributeEthRewards(uint256 ethSent) private { + if (!TransferHelperUtils.safeSendETH(minterConfig.zoraRewardRecipientAddress, ethSent, TransferHelperUtils.FUNDS_SEND_NORMAL_GAS_LIMIT)) { + revert FailedToSendEthReward(); + } + } + /// @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 @@ -192,7 +201,11 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint address currency, address mintReferral, string calldata comment - ) external nonReentrant { + ) external payable nonReentrant { + if (msg.value != minterConfig.ethReward) { + revert InvalidValue(); + } + SalesConfig storage config = salesConfigs[tokenAddress][tokenId]; if (config.currency == address(0) || config.currency != currency) { @@ -223,6 +236,8 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint _distributeRewards(totalReward, currency, tokenId, tokenAddress, mintReferral); + _distributeEthRewards(msg.value); + IERC20(config.currency).safeTransfer(config.fundsRecipient, totalValue - totalReward); if (bytes(comment).length > 0) { @@ -231,8 +246,13 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint } /// @notice The percentage of the total value that is distributed as rewards - function totalRewardPct() external pure returns (uint256) { - return TOTAL_REWARD_PCT; + function totalRewardPct() external view returns (uint256) { + return minterConfig.rewardRecipientPercentage; + } + + /// @notice The amount of ETH distributed as rewards + function ethRewardAmount() external view returns (uint256) { + return minterConfig.ethReward; } /// @notice The URI of the contract @@ -247,20 +267,19 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint /// @notice The version of the contract function contractVersion() external pure returns (string memory) { - return "1.0.0"; + return "2.0.0"; } /// @notice Sets the sale config for a given token + /// @param tokenId The ID of the token to set the sale config for + /// @param salesConfig The sale config to set function setSale(uint256 tokenId, SalesConfig memory salesConfig) external { + _requireNotAddressZero(salesConfig.currency); + _requireNotAddressZero(salesConfig.fundsRecipient); + 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; @@ -269,6 +288,7 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint } /// @notice Deletes the sale config for a given token + /// @param tokenId The ID of the token to reset the sale config for function resetSale(uint256 tokenId) external override { delete salesConfigs[msg.sender][tokenId]; @@ -277,6 +297,8 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint } /// @notice Returns the sale config for a given token + /// @param tokenContract The TokenContract address + /// @param tokenId The ID of the token to get the sale config for function sale(address tokenContract, uint256 tokenId) external view returns (SalesConfig memory) { return salesConfigs[tokenContract][tokenId]; } @@ -291,19 +313,26 @@ contract ERC20Minter is ReentrancyGuard, IERC20Minter, SaleStrategy, LimitedMint 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(); - } + function _setERC20MinterConfig(ERC20MinterConfig memory _config) internal { + _requireNotAddressZero(_config.zoraRewardRecipientAddress); - if (recipient == address(0)) { - revert AddressZero(); + if (_config.rewardRecipientPercentage > 100) { + revert InvalidValue(); } - emit ZoraRewardsRecipientSet(zoraRewardRecipientAddress, recipient); + minterConfig = _config; + emit ERC20MinterConfigSet(_config); + } - zoraRewardRecipientAddress = recipient; + /// @notice Sets the ERC20MinterConfig + /// @param config The ERC20MinterConfig to set + function setERC20MinterConfig(ERC20MinterConfig memory config) external onlyOwner { + _setERC20MinterConfig(config); + } + + function _requireNotAddressZero(address _address) internal { + if (_address == address(0)) { + revert AddressZero(); + } } } diff --git a/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol b/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol index 6a1509ef..2d430d81 100644 --- a/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol +++ b/packages/1155-contracts/src/minters/erc20/ERC20MinterRewards.sol @@ -4,7 +4,6 @@ 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 diff --git a/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol b/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol index d5cbe0a4..b8e9f969 100644 --- a/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol +++ b/packages/1155-contracts/test/minters/erc20/ERC20Minter.t.sol @@ -23,7 +23,9 @@ contract ERC20MinterTest is Test { address internal fundsRecipient; address internal createReferral; address internal mintReferral; + address internal owner; ERC20Minter internal minter; + IERC20Minter.ERC20MinterConfig internal minterConfig; uint256 internal constant TOTAL_REWARD_PCT = 5; uint256 immutable BPS_TO_PERCENT = 100; @@ -32,6 +34,7 @@ contract ERC20MinterTest is Test { 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; + uint256 internal constant ethReward = 0.000111 ether; event ERC20RewardsDeposit( address indexed createReferral, @@ -47,7 +50,9 @@ contract ERC20MinterTest is Test { uint256 zoraReward ); - event ERC20MinterInitialized(uint256 rewardPercentage); + event ERC20MinterConfigSet(IERC20Minter.ERC20MinterConfig config); + + event OwnerSet(address indexed prevOwner, address indexed owner); event MintComment(address indexed sender, address indexed tokenContract, uint256 indexed tokenId, uint256 quantity, string comment); @@ -57,6 +62,7 @@ contract ERC20MinterTest is Test { fundsRecipient = makeAddr("fundsRecipient"); createReferral = makeAddr("createReferral"); mintReferral = makeAddr("mintReferral"); + owner = makeAddr("owner"); bytes[] memory emptyData = new bytes[](0); ProtocolRewards protocolRewards = new ProtocolRewards(); @@ -65,18 +71,25 @@ contract ERC20MinterTest is Test { target = ZoraCreator1155Impl(payable(address(proxy))); target.initialize("test", "test", ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)), admin, emptyData); minter = new ERC20Minter(); - minter.initialize(zora); + minter.initialize(zora, owner, 5, ethReward); vm.prank(admin); currency = new ERC20PresetMinterPauser("Test currency", "TEST"); + minterConfig = minter.getERC20MinterConfig(); } - function setUpTargetSale(uint256 price, address tokenFundsRecipient, address tokenCurrency, uint256 quantity) internal returns (uint256) { + function setUpTargetSale( + uint256 price, + address tokenFundsRecipient, + address tokenCurrency, + uint256 quantity, + ERC20Minter minterContract + ) 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.addPermission(newTokenId, address(minterContract), target.PERMISSION_BIT_MINTER()); target.callSale( newTokenId, - minter, + minterContract, abi.encodeWithSelector( ERC20Minter.setSale.selector, newTokenId, @@ -97,21 +110,36 @@ contract ERC20MinterTest is Test { function test_ERC20MinterInitializeEventIsEmitted() external { vm.expectEmit(true, true, true, true); - emit ERC20MinterInitialized(TOTAL_REWARD_PCT); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: zora, + rewardRecipientPercentage: 5, + ethReward: ethReward + }); + emit ERC20MinterConfigSet(newConfig); + minter = new ERC20Minter(); - minter.initialize(zora); + minter.initialize(zora, owner, 5, ethReward); } - function test_ERC20MinterCannotInitializeWithAddressZero() external { + function test_ERC20MinterZoraAddrCannotInitializeWithAddressZero() external { minter = new ERC20Minter(); vm.expectRevert(abi.encodeWithSignature("AddressZero()")); - minter.initialize(address(0)); + minter.initialize(address(0), owner, 5, ethReward); + } + + function test_ERC20MinterOwnerAddrCannotInitializeWithAddressZero() external { + minter = new ERC20Minter(); + + vm.expectRevert(abi.encodeWithSignature("OWNER_CANNOT_BE_ZERO_ADDRESS()")); + minter.initialize(zora, address(0), 5, ethReward); } - function test_ERC20MinterCannotReinitialize() external { - vm.expectRevert(abi.encodeWithSignature("AlreadyInitialized()")); - minter.initialize(address(this)); + function test_ERC20MinterRewardPercentageCannotBeGreaterThan100() external { + minter = new ERC20Minter(); + + vm.expectRevert(abi.encodeWithSignature("InvalidValue()")); + minter.initialize(zora, owner, 101, ethReward); } function test_ERC20MinterContractName() external { @@ -119,7 +147,15 @@ contract ERC20MinterTest is Test { } function test_ERC20MinterContractVersion() external { - assertEq(minter.contractVersion(), "1.0.0"); + assertEq(minter.contractVersion(), "2.0.0"); + } + + function test_ERC20MinterAlreadyInitalized() external { + minter = new ERC20Minter(); + minter.initialize(zora, owner, 5, ethReward); + + vm.expectRevert(abi.encodeWithSignature("INITIALIZABLE_CONTRACT_ALREADY_INITIALIZED()")); + minter.initialize(zora, owner, 5, ethReward); } function test_ERC20MinterSaleConfigPriceTooLow() external { @@ -140,7 +176,7 @@ contract ERC20MinterTest is Test { saleStart: 0, saleEnd: type(uint64).max, maxTokensPerAddress: 0, - fundsRecipient: address(0), + fundsRecipient: address(0x123), currency: address(currency) }) ) @@ -201,10 +237,12 @@ contract ERC20MinterTest is Test { } function test_ERC20MinterRevertIfCurrencyDoesNotMatchSalesConfigCurrency() external { - setUpTargetSale(10_000, fundsRecipient, address(currency), 1); + setUpTargetSale(10_000, fundsRecipient, address(currency), 1, minter); + + vm.deal(tokenRecipient, ethReward); vm.expectRevert(abi.encodeWithSignature("InvalidCurrency()")); - minter.mint(tokenRecipient, 1, address(target), 1, 1, makeAddr("0x123"), address(0), ""); + minter.mint{value: ethReward}(tokenRecipient, 1, address(target), 1, 1, makeAddr("0x123"), address(0), ""); } function test_ERC20MinterRequestMintInvalid() external { @@ -229,7 +267,7 @@ contract ERC20MinterTest is Test { function test_ERC20MinterSaleFlow() external { uint96 pricePerToken = 10_000; uint256 quantity = 2; - uint256 newTokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + uint256 newTokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity, minter); vm.deal(tokenRecipient, 1 ether); vm.prank(admin); @@ -239,8 +277,10 @@ contract ERC20MinterTest is Test { vm.prank(tokenRecipient); currency.approve(address(minter), totalValue); + vm.deal(tokenRecipient, ethReward); + vm.startPrank(tokenRecipient); - minter.mint(tokenRecipient, quantity, address(target), newTokenId, pricePerToken * quantity, address(currency), mintReferral, ""); + minter.mint{value: ethReward}(tokenRecipient, quantity, address(target), newTokenId, pricePerToken * quantity, address(currency), mintReferral, ""); vm.stopPrank(); assertEq(target.balanceOf(tokenRecipient, newTokenId), quantity); @@ -257,14 +297,15 @@ contract ERC20MinterTest is Test { currency.balanceOf(createReferral), totalValue ); + assertEq(address(zora).balance, ethReward); } function test_ERC20MinterSaleWithRewardsAddresses() external { uint96 pricePerToken = 100000000000000000; // 0.1 when converted from wei uint256 quantity = 5; - uint256 newTokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + uint256 newTokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity, minter); - vm.deal(tokenRecipient, 1 ether); + vm.deal(tokenRecipient, ethReward); vm.prank(admin); uint256 totalValue = pricePerToken * quantity; currency.mint(address(tokenRecipient), totalValue); @@ -273,7 +314,7 @@ contract ERC20MinterTest is Test { currency.approve(address(minter), totalValue); vm.startPrank(tokenRecipient); - minter.mint(tokenRecipient, quantity, address(target), newTokenId, pricePerToken * quantity, address(currency), mintReferral, ""); + minter.mint{value: ethReward}(tokenRecipient, quantity, address(target), newTokenId, pricePerToken * quantity, address(currency), mintReferral, ""); vm.stopPrank(); assertEq(target.balanceOf(tokenRecipient, newTokenId), quantity); @@ -291,21 +332,25 @@ contract ERC20MinterTest is Test { ); } - function test_ERC20MinterSaleFuzz(uint96 pricePerToken, uint256 quantity) external { + function test_ERC20MinterSaleFuzz(uint96 pricePerToken, uint256 quantity, uint8 rewardPct, uint256 zoraEthReward) external { vm.assume(quantity > 0 && quantity < 1_000_000_000); vm.assume(pricePerToken > 10_000 && pricePerToken < type(uint96).max); + vm.assume(rewardPct > 0 && rewardPct < 100); + vm.assume(zoraEthReward > 0 ether && zoraEthReward < 1 ether); - uint256 tokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + ERC20Minter newMinter = new ERC20Minter(); + newMinter.initialize(address(zora), owner, rewardPct, zoraEthReward); + + uint256 tokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity, newMinter); - 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); + currency.approve(address(newMinter), totalValue); - uint256 reward = (totalValue * TOTAL_REWARD_PCT) / BPS_TO_PERCENT; + uint256 reward = (totalValue * rewardPct) / 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; @@ -326,7 +371,10 @@ contract ERC20MinterTest is Test { firstMinterReward, zoraReward ); - minter.mint(tokenRecipient, quantity, address(target), tokenId, pricePerToken * quantity, address(currency), mintReferral, ""); + vm.deal(tokenRecipient, zoraEthReward); + + uint256 amount = pricePerToken * quantity; + newMinter.mint{value: zoraEthReward}(tokenRecipient, quantity, address(target), tokenId, amount, address(currency), mintReferral, ""); vm.stopPrank(); assertEq(target.balanceOf(tokenRecipient, tokenId), quantity); @@ -343,6 +391,7 @@ contract ERC20MinterTest is Test { currency.balanceOf(admin), totalValue ); + assertEq(address(zora).balance, zoraEthReward); } function test_ERC20MinterCreateReferral() public { @@ -355,7 +404,7 @@ contract ERC20MinterTest is Test { assertEq(targetCreateReferral, createReferral); address fallbackCreateReferral = minter.getCreateReferral(address(this), 1); - assertEq(fallbackCreateReferral, minter.zoraRewardRecipientAddress()); + assertEq(fallbackCreateReferral, minterConfig.zoraRewardRecipientAddress); } function test_ERC20MinterFirstMinterFallback() public { @@ -363,39 +412,113 @@ contract ERC20MinterTest is Test { uint256 quantity = 11; uint256 totalValue = pricePerToken * quantity; - uint256 tokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity); + uint256 tokenId = setUpTargetSale(pricePerToken, fundsRecipient, address(currency), quantity, minter); address collector = makeAddr("collector"); vm.prank(admin); currency.mint(collector, totalValue); + vm.deal(collector, ethReward); + vm.startPrank(collector); currency.approve(address(minter), totalValue); - minter.mint(collector, quantity, address(target), tokenId, totalValue, address(currency), address(0), ""); + minter.mint{value: ethReward}(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()); + assertEq(fallbackFirstMinter, minterConfig.zoraRewardRecipientAddress); } function test_ERC20MinterSetZoraRewardsRecipient() public { - vm.prank(zora); - minter.setZoraRewardsRecipient(address(this)); - - assertEq(minter.zoraRewardRecipientAddress(), address(this)); + vm.prank(owner); + vm.expectEmit(true, true, true, true); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: address(this), + rewardRecipientPercentage: 5, + ethReward: ethReward + }); + emit ERC20MinterConfigSet(newConfig); + minter.setERC20MinterConfig(newConfig); + + minterConfig = minter.getERC20MinterConfig(); + assertEq(minterConfig.zoraRewardRecipientAddress, address(this)); } function test_ERC20MinterOnlyRecipientAddressCanSet() public { - vm.expectRevert(abi.encodeWithSignature("OnlyZoraRewardsRecipient()")); - minter.setZoraRewardsRecipient(address(this)); + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: address(this), + rewardRecipientPercentage: 5, + ethReward: ethReward + }); + minter.setERC20MinterConfig(newConfig); } function test_ERC20MinterCannotSetRecipientToZero() public { vm.expectRevert(abi.encodeWithSignature("AddressZero()")); + vm.prank(owner); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: address(0), + rewardRecipientPercentage: 5, + ethReward: ethReward + }); + minter.setERC20MinterConfig(newConfig); + } + + function test_ERC20SetRewardRecipientPercentage(uint256 percentageFuzz) public { + vm.assume(percentageFuzz > 0 && percentageFuzz < 100); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSignature("InvalidValue()")); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: zora, + rewardRecipientPercentage: 101, + ethReward: ethReward + }); + minter.setERC20MinterConfig(newConfig); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + newConfig = IERC20Minter.ERC20MinterConfig({zoraRewardRecipientAddress: zora, rewardRecipientPercentage: percentageFuzz, ethReward: ethReward}); + emit ERC20MinterConfigSet(newConfig); + minter.setERC20MinterConfig(newConfig); + } + + function test_ERC20MinterSetEthReward(uint256 ethRewardFuzz) public { + vm.assume(ethRewardFuzz >= 0 ether && ethRewardFuzz < 10 ether); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: zora, + rewardRecipientPercentage: minterConfig.rewardRecipientPercentage, + ethReward: ethRewardFuzz + }); + emit ERC20MinterConfigSet(newConfig); + minter.setERC20MinterConfig(newConfig); + } + + function test_ERC20MinterSetOwner() public { vm.prank(zora); - minter.setZoraRewardsRecipient(address(0)); + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + IERC20Minter.ERC20MinterConfig memory newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: zora, + rewardRecipientPercentage: minterConfig.rewardRecipientPercentage, + ethReward: ethReward + }); + minter.setERC20MinterConfig(newConfig); + + vm.prank(owner); + vm.expectEmit(true, true, true, true); + newConfig = IERC20Minter.ERC20MinterConfig({ + zoraRewardRecipientAddress: zora, + rewardRecipientPercentage: minterConfig.rewardRecipientPercentage, + ethReward: ethReward + }); + emit ERC20MinterConfigSet(newConfig); + minter.setERC20MinterConfig(newConfig); } } diff --git a/packages/1155-contracts/test/premint/PremintERC20.t.sol b/packages/1155-contracts/test/premint/PremintERC20.t.sol index 13d8d517..cb54c785 100644 --- a/packages/1155-contracts/test/premint/PremintERC20.t.sol +++ b/packages/1155-contracts/test/premint/PremintERC20.t.sol @@ -23,9 +23,11 @@ import {Zora1155} from "../../src/proxies/Zora1155.sol"; contract PremintERC20Test is Test { uint256 internal creatorPK; + uint256 internal ethReward; address internal creator; address internal zora; address internal collector; + address internal owner; ProtocolRewards internal protocolRewards; ERC20Minter internal erc20Minter; @@ -42,10 +44,12 @@ contract PremintERC20Test is Test { (creator, creatorPK) = makeAddrAndKey("creator"); collector = makeAddr("collector"); zora = makeAddr("zora"); + owner = makeAddr("owner"); + ethReward = 0.000111 ether; mockErc20 = new ERC20PresetMinterPauser("Mock", "MOCK"); erc20Minter = new ERC20Minter(); - erc20Minter.initialize(zora); + erc20Minter.initialize(zora, owner, 5, ethReward); protocolRewards = new ProtocolRewards(); zora1155Impl = address(new ZoraCreator1155Impl(zora, address(new UpgradeGate()), address(protocolRewards), address(0))); @@ -61,6 +65,8 @@ contract PremintERC20Test is Test { } function testPremintERC20() public { + // TODO: fix when we have a way to support payable erc20 mints + vm.skip(true); ContractCreationConfig memory contractConfig = ContractCreationConfig({contractAdmin: creator, contractName: "test", contractURI: "test.uri"}); Erc20TokenCreationConfigV1 memory tokenConfig = Erc20TokenCreationConfigV1({ @@ -90,6 +96,8 @@ contract PremintERC20Test is Test { uint256 quantityToMint = 1; uint256 totalValue = tokenConfig.pricePerToken * quantityToMint; + + vm.deal(collector, ethReward); mockErc20.mint(collector, totalValue); vm.prank(collector); diff --git a/packages/creator-subgraph/config/zora-sepolia.yaml b/packages/creator-subgraph/config/zora-sepolia.yaml index c1aa227a..abd1d668 100644 --- a/packages/creator-subgraph/config/zora-sepolia.yaml +++ b/packages/creator-subgraph/config/zora-sepolia.yaml @@ -32,3 +32,6 @@ erc20Minter: - address: "0x777777E8850d8D6d98De2B5f64fae401F96eFF31" startBlock: "6695930" version: "1" + - address: "0xed3f8CD54DfE67495337ceA7F6fb9d790d2b493F" + startBlock: "8639880" + version: "2" diff --git a/packages/creator-subgraph/scripts/extract-abis.mjs b/packages/creator-subgraph/scripts/extract-abis.mjs index 54a2f871..2ccc03b7 100644 --- a/packages/creator-subgraph/scripts/extract-abis.mjs +++ b/packages/creator-subgraph/scripts/extract-abis.mjs @@ -10,6 +10,7 @@ import { zoraCreatorRedeemMinterFactoryABI, zoraCreatorRedeemMinterStrategyABI, protocolRewardsABI, + erc20MinterABI, } from "@zoralabs/zora-1155-contracts"; import { zoraMints1155ABI, @@ -63,3 +64,5 @@ output_abi("ProtocolRewards", protocolRewardsABI); output_abi("ZoraMints1155", zoraMints1155ABI); output_abi("ZoraMintsManagerImpl", zoraMintsManagerImplABI); + +output_abi("ERC20Minter", erc20MinterABI); diff --git a/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts b/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts index 4a4bb102..6e5db43e 100644 --- a/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts +++ b/packages/creator-subgraph/src/ERC1155Mappings/ERC20MinterMappings.ts @@ -2,7 +2,7 @@ import { MintComment as ERC20MintComment, SaleSet, ERC20RewardsDeposit as ERC20RewardsDepositEvent, -} from "../../generated/ERC20Minter/ERC20Minter"; +} from "../../generated/ERC20Minter2/ERC20Minter"; import { BigInt } from "@graphprotocol/graph-ts"; import { getSalesConfigKey } from "../common/getSalesConfigKey"; import { getTokenId } from "../common/getTokenId"; diff --git a/packages/creator-subgraph/subgraph.template.yaml b/packages/creator-subgraph/subgraph.template.yaml index 8494fa46..516d317e 100644 --- a/packages/creator-subgraph/subgraph.template.yaml +++ b/packages/creator-subgraph/subgraph.template.yaml @@ -195,7 +195,7 @@ dataSources: handler: handleMintComment {{/mintsManager}} {{#erc20Minter}} - - name: ERC20Minter + - name: ERC20Minter{{version}} kind: ethereum/contract network: {{network}} source: