diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index a2acad38..00000000 --- a/.husky/pre-push +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -pnpm run solhint \ No newline at end of file diff --git a/contracts/modules/SuperMinterE.sol b/contracts/modules/SuperMinterE.sol index 0e997564..bf28dbd7 100644 --- a/contracts/modules/SuperMinterE.sol +++ b/contracts/modules/SuperMinterE.sol @@ -200,7 +200,7 @@ contract SuperMinterE is ISuperMinterE, EIP712 { /** * @dev A mapping of `platform` => `token` => `feesAccrued`. */ - mapping(address => address(address => uint256)) public platformERC20FeesAccrued; + mapping(address => mapping(address => uint256)) public platformERC20FeesAccrued; /** * @dev A mapping of `platform` => `feeRecipient`. @@ -215,7 +215,7 @@ contract SuperMinterE is ISuperMinterE, EIP712 { /** * @dev A mapping of `affiliate` => `token` => `feesAccrued`. */ - mapping(address => address(address => uint256)) public affiliateERC20FeesAccrued; + mapping(address => mapping(address => uint256)) public affiliateERC20FeesAccrued; /** * @dev A mapping of `platform` => `price`. @@ -318,12 +318,15 @@ contract SuperMinterE is ISuperMinterE, EIP712 { d.mode = c.mode; d.flags = _MINT_CREATED_FLAG; d.next = editionHead.head; + d.erc20 = c.erc20; editionHead.head = uint16((uint256(c.tier) << 8) | uint256(scheduleNum)); // Skip writing zeros, to avoid cold SSTOREs. if (c.affiliateMerkleRoot != bytes32(0)) d.affiliateMerkleRoot = c.affiliateMerkleRoot; if (c.merkleRoot != bytes32(0)) d.merkleRoot = c.merkleRoot; + if (c.erc20 != address(0) && c.erc20.code.length == 0) revert ERC20DoesNotExist(); + emit MintCreated(c.edition, c.tier, scheduleNum, c); } } @@ -339,12 +342,13 @@ contract SuperMinterE is ISuperMinterE, EIP712 { _requireMintOpen(d); // Perform the sub workflows depending on the mint mode. - uint8 mode = d.mode; - if (mode == VERIFY_MERKLE) _verifyMerkle(d, p); - else if (mode == VERIFY_SIGNATURE) _verifyAndClaimSignature(d, p); - else if (mode == PLATFORM_AIRDROP) revert InvalidMode(); - - _incrementMinted(mode, d, p); + { + uint8 mode = d.mode; + if (mode == VERIFY_MERKLE) _verifyMerkle(d, p); + else if (mode == VERIFY_SIGNATURE) _verifyAndClaimSignature(d, p); + else if (mode == PLATFORM_AIRDROP) revert InvalidMode(); + _incrementMinted(mode, d, p); + } /* ----------------- COMPUTE AND ACCRUE FEES ---------------- */ @@ -356,7 +360,10 @@ contract SuperMinterE is ISuperMinterE, EIP712 { TotalPriceAndFees memory f = _totalPriceAndFees(p.tier, d, p.quantity, p.signedPrice, l.affiliated); - if (msg.value != f.total) revert WrongPayment(msg.value, f.total); // Require exact payment. + l.erc20 = d.erc20; + if (l.erc20 == address(0)) { + if (msg.value != f.total) revert WrongPayment(msg.value, f.total); // Require exact payment. + } l.finalArtistFee = f.finalArtistFee; l.finalPlatformFee = f.finalPlatformFee; @@ -367,10 +374,18 @@ contract SuperMinterE is ISuperMinterE, EIP712 { // Overflow not possible since all fees are uint96s. unchecked { if (l.finalAffiliateFee != 0) { - affiliateFeesAccrued[p.affiliate] += l.finalAffiliateFee; + if (l.erc20 == address(0)) { + affiliateFeesAccrued[p.affiliate] += l.finalAffiliateFee; + } else { + affiliateERC20FeesAccrued[p.affiliate][l.erc20] += l.finalAffiliateFee; + } } if (l.finalPlatformFee != 0) { - platformFeesAccrued[d.platform] += l.finalPlatformFee; + if (l.erc20 == address(0)) { + platformFeesAccrued[d.platform] += l.finalPlatformFee; + } else { + platformERC20FeesAccrued[d.platform][l.erc20] += l.finalPlatformFee; + } } } @@ -378,13 +393,22 @@ contract SuperMinterE is ISuperMinterE, EIP712 { ISoundEditionV2_1 edition = ISoundEditionV2_1(p.edition); l.quantity = p.quantity; - l.fromTokenId = edition.mint{ value: l.finalArtistFee }(p.tier, p.to, p.quantity); l.allowlisted = p.allowlisted; l.allowlistedQuantity = p.allowlistedQuantity; l.signedClaimTicket = p.signedClaimTicket; l.requiredEtherValue = f.total; l.unitPrice = f.unitPrice; + if (l.erc20 == address(0)) { + l.fromTokenId = edition.mint{ value: l.finalArtistFee }(p.tier, p.to, p.quantity); + } else { + l.fromTokenId = edition.mint(p.tier, p.to, p.quantity); + SafeTransferLib.safeTransferFrom(l.erc20, msg.sender, address(edition), l.finalArtistFee); + unchecked { + uint256 feesToThis = l.finalPlatformFee + l.finalAffiliateFee; + SafeTransferLib.safeTransferFrom(l.erc20, msg.sender, address(this), feesToThis); + } + } emit Minted(p.edition, p.tier, p.scheduleNum, p.to, l, p.attributionId); return l.fromTokenId; @@ -588,6 +612,18 @@ contract SuperMinterE is ISuperMinterE, EIP712 { } } + /** + * @inheritdoc ISuperMinterE + */ + function withdrawERC20ForAffiliate(address erc20, address affiliate) public { + uint256 accrued = affiliateERC20FeesAccrued[affiliate][erc20]; + if (accrued != 0) { + affiliateERC20FeesAccrued[affiliate][erc20] = 0; + SafeTransferLib.safeTransfer(erc20, affiliate, accrued); + emit AffiliateERC20FeesWithdrawn(affiliate, erc20, accrued); + } + } + /** * @inheritdoc ISuperMinterE */ @@ -602,6 +638,20 @@ contract SuperMinterE is ISuperMinterE, EIP712 { } } + /** + * @inheritdoc ISuperMinterE + */ + function withdrawERC20ForPlatform(address erc20, address platform) public { + address recipient = platformFeeAddress[platform]; + _validatePlatformFeeAddress(recipient); + uint256 accrued = platformERC20FeesAccrued[platform][erc20]; + if (accrued != 0) { + platformERC20FeesAccrued[platform][erc20] = 0; + SafeTransferLib.safeTransfer(erc20, recipient, accrued); + emit PlatformERC20FeesWithdrawn(platform, erc20, accrued); + } + } + // Platform fee functions: // ----------------------- // These functions enable any caller to set their own platform fees. diff --git a/contracts/modules/interfaces/ISuperMinterE.sol b/contracts/modules/interfaces/ISuperMinterE.sol index f5314510..2312531e 100644 --- a/contracts/modules/interfaces/ISuperMinterE.sol +++ b/contracts/modules/interfaces/ISuperMinterE.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.16; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; /** - * @title ISuperMinterV2 + * @title ISuperMinterE * @notice The interface for the generalized minter. */ -interface ISuperMinterV2 is IERC165 { +interface ISuperMinterE is IERC165 { // ============================================================= // STRUCTS // ============================================================= @@ -158,6 +158,8 @@ interface ISuperMinterV2 is IERC165 { // The final platform fee // (inclusive of `finalPlatformReward`, `perTxFlat`, sum of `perMintBPS`). uint256 finalPlatformFee; + // The erc20 token for payment, if any. + address erc20; } /** @@ -373,6 +375,14 @@ interface ISuperMinterV2 is IERC165 { */ event AffiliateFeesWithdrawn(address indexed affiliate, uint256 accrued); + /** + * @dev Emitted when affiliate ERC20 fees are withdrawn. + * @param affiliate The recipient of the fees. + * @param erc20 The erc20 token address. + * @param accrued The amount of Ether accrued and withdrawn. + */ + event AffiliateERC20FeesWithdrawn(address indexed affiliate, address indexed erc20, uint256 accrued); + /** * @dev Emitted when platform fees are withdrawn. * @param platform The platform address. @@ -380,6 +390,14 @@ interface ISuperMinterV2 is IERC165 { */ event PlatformFeesWithdrawn(address indexed platform, uint256 accrued); + /** + * @dev Emitted when platform ERC20 fees are withdrawn. + * @param platform The platform address. + * @param erc20 The erc20 token address. + * @param accrued The amount of Ether accrued and withdrawn. + */ + event PlatformERC20FeesWithdrawn(address indexed platform, address indexed erc20, uint256 accrued); + /** * @dev Emitted when the platform fee recipient address is updated. * @param platform The platform address. @@ -545,6 +563,11 @@ interface ISuperMinterV2 is IERC165 { */ error NotConfigurable(); + /** + * @dev The ERC20 contract does not exist. + */ + error ERC20DoesNotExist(); + // ============================================================= // PUBLIC / EXTERNAL WRITE FUNCTIONS // ============================================================= @@ -699,17 +722,31 @@ interface ISuperMinterV2 is IERC165 { ) external; /** - * @dev Withdraws all accrued fees of the affiliate, to the affiliate. + * @dev Withdraws all accrued ETH fees of the affiliate, to the affiliate. * @param affiliate The affiliate address. */ function withdrawForAffiliate(address affiliate) external; /** - * @dev Withdraws all accrued fees of the platform, to the their fee address. + * @dev Withdraws all accrued ETH fees of the platform, to the their fee address. * @param platform The platform address. */ function withdrawForPlatform(address platform) external; + /** + * @dev Withdraws all accrued ERC20 fees of the affiliate, to the affiliate. + * @param erc20 The erc20 token address. + * @param affiliate The affiliate address. + */ + function withdrawERC20ForAffiliate(address erc20, address affiliate) external; + + /** + * @dev Withdraws all accrued ERC20 fees of the platform, to the their fee address. + * @param erc20 The erc20 token address. + * @param platform The platform address. + */ + function withdrawERC20ForPlatform(address erc20, address platform) external; + /** * @dev Allows the caller, as a platform, to set their fee address * @param recipient The platform fee address of the caller.