diff --git a/contracts/modules/SuperMinterE.sol b/contracts/modules/SuperMinterE.sol new file mode 100644 index 00000000..d8184e02 --- /dev/null +++ b/contracts/modules/SuperMinterE.sol @@ -0,0 +1,1354 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { ISoundEditionV2_1 } from "@core/interfaces/ISoundEditionV2_1.sol"; +import { ISuperMinterE } from "@modules/interfaces/ISuperMinterE.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { EIP712 } from "solady/utils/EIP712.sol"; +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { LibBitmap } from "solady/utils/LibBitmap.sol"; +import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; +import { LibZip } from "solady/utils/LibZip.sol"; +import { LibMap } from "solady/utils/LibMap.sol"; +import { DelegateCashLib } from "@modules/utils/DelegateCashLib.sol"; +import { LibOps } from "@core/utils/LibOps.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; + +/** + * @title SuperMinterE + * @dev The `SuperMinterE` class is a generalized minter that also supports ERC20 tokens. + */ +contract SuperMinterE is ISuperMinterE, EIP712 { + using LibBitmap for *; + using MerkleProofLib for *; + using LibMap for *; + + // ============================================================= + // STRUCTS + // ============================================================= + + /** + * @dev A struct to hold the mint data in storage. + */ + struct MintData { + // The platform address. + address platform; + // The start time of the mint. + uint32 startTime; + // The end time of the mint. + uint32 endTime; + // The maximum number of tokens an account can mint in this mint. + uint32 maxMintablePerAccount; + // The price per token. + uint128 price; + // The maximum tokens mintable. + uint32 maxMintable; + // The total number of tokens minted. + uint32 minted; + // The affiliate fee BPS. + uint16 affiliateFeeBPS; + // The offset to the next mint data in the linked list. + uint16 next; + // The head of the mint data linked list. + // Only stored in the 0-th mint data per edition. + uint16 head; + // The total number of mint data. + // Only stored in the 0-th mint data per edition. + uint16 numMintData; + // The total number of mints for the edition-tier. + // Only stored in the 0-th mint data per edition-tier. + uint8 nextScheduleNum; + // The mode of the mint. + uint8 mode; + // The packed boolean flags. + uint8 flags; + // The affiliate Merkle root, if any. + bytes32 affiliateMerkleRoot; + // The Merkle root hash, required if `mode` is `VERIFY_MERKLE`. + bytes32 merkleRoot; + // The ERC20 token to denominate the fees in. + address erc20; + } + + // ============================================================= + // CONSTANTS + // ============================================================= + + /** + * @dev The GA tier. Which is 0. + */ + uint8 public constant GA_TIER = 0; + + /** + * @dev For EIP-712 signature digest calculation. + */ + bytes32 public constant MINT_TO_TYPEHASH = + // prettier-ignore + keccak256( + "MintTo(" + "address edition," + "uint8 tier," + "uint8 scheduleNum," + "address to," + "uint32 signedQuantity," + "uint32 signedClaimTicket," + "uint128 signedPrice," + "uint32 signedDeadline," + "address affiliate" + ")" + ); + + /** + * @dev For EIP-712 platform airdrop signature digest calculation. + */ + bytes32 public constant PLATFORM_AIRDROP_TYPEHASH = + // prettier-ignore + keccak256( + "PlatformAirdrop(" + "address edition," + "uint8 tier," + "uint8 scheduleNum," + "address[] to," + "uint32 signedQuantity," + "uint32 signedClaimTicket," + "uint32 signedDeadline" + ")" + ); + + /** + * @dev For EIP-712 signature digest calculation. + */ + bytes32 public constant DOMAIN_TYPEHASH = _DOMAIN_TYPEHASH; + + /** + * @dev The default value for options. + */ + uint8 public constant DEFAULT = 0; + + /** + * @dev The Merkle drop mint mode. + */ + uint8 public constant VERIFY_MERKLE = 1; + + /** + * @dev The Signature mint mint mode. + */ + uint8 public constant VERIFY_SIGNATURE = 2; + + /** + * @dev The platform airdrop mint mode. + */ + uint8 public constant PLATFORM_AIRDROP = 3; + + /** + * @dev The denominator of all BPS calculations. + */ + uint16 public constant BPS_DENOMINATOR = LibOps.BPS_DENOMINATOR; + + /** + * @dev The maximum affiliate fee BPS. + */ + uint16 public constant MAX_AFFILIATE_FEE_BPS = 1000; + + /** + * @dev The maximum platform per-mint fee BPS. + */ + uint16 public constant MAX_PLATFORM_PER_MINT_FEE_BPS = 1000; + + /** + * @dev The maximum per-mint reward. Applies to artists, affiliates, platform. + */ + uint128 public constant MAX_PER_MINT_REWARD = 0.1 ether; + + /** + * @dev The maximum platform per-transaction flat fee. + */ + uint128 public constant MAX_PLATFORM_PER_TX_FLAT_FEE = 0.1 ether; + + /** + * @dev The boolean flag on whether the mint has been created. + */ + uint8 internal constant _MINT_CREATED_FLAG = 1 << 0; + + /** + * @dev The boolean flag on whether the mint is paused. + */ + uint8 internal constant _MINT_PAUSED_FLAG = 1 << 1; + + /** + * @dev The boolean flag on whether the signer is the platform's signer. + */ + uint8 internal constant _USE_PLATFORM_SIGNER_FLAG = 1 << 2; + + /** + * @dev The index for the per-platform default fee config. + * We use 256, as the tier is uint8, which ranges from 0 to 255. + */ + uint16 internal constant _DEFAULT_FEE_CONFIG_INDEX = 256; + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev A mapping of `platform` => `feesAccrued`. + */ + mapping(address => uint256) public platformFeesAccrued; + + /** + * @dev A mapping of `platform` => `token` => `feesAccrued`. + */ + mapping(address => mapping(address => uint256)) public platformERC20FeesAccrued; + + /** + * @dev A mapping of `platform` => `feeRecipient`. + */ + mapping(address => address) public platformFeeAddress; + + /** + * @dev A mapping of `affiliate` => `feesAccrued`. + */ + mapping(address => uint256) public affiliateFeesAccrued; + + /** + * @dev A mapping of `affiliate` => `token` => `feesAccrued`. + */ + mapping(address => mapping(address => uint256)) public affiliateERC20FeesAccrued; + + /** + * @dev A mapping of `platform` => `price`. + */ + mapping(address => uint128) public gaPrice; + + /** + * @dev A mapping of `platform` => `platformSigner`. + */ + mapping(address => address) public platformSigner; + + /** + * @dev A mapping of `mintId` => `mintData`. + */ + mapping(uint256 => MintData) internal _mintData; + + /** + * @dev A mapping of `platformTierId` => `platformFeeConfig`. + */ + mapping(uint256 => PlatformFeeConfig) internal _platformFeeConfigs; + + /** + * @dev A mapping of `to` => `mintId` => `numberMinted`. + */ + mapping(address => LibMap.Uint32Map) internal _numberMinted; + + /** + * @dev A mapping of `mintId` => `signedClaimedTicket` => `claimed`. + */ + mapping(uint256 => LibBitmap.Bitmap) internal _claimsBitmaps; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISuperMinterE + */ + function createEditionMint(MintCreation memory c) public returns (uint8 scheduleNum) { + _requireOnlyEditionOwnerOrAdmin(c.edition); + + _validateAffiliateFeeBPS(c.affiliateFeeBPS); + + uint8 mode = c.mode; + + if (mode == DEFAULT) { + c.merkleRoot = bytes32(0); + } else if (mode == VERIFY_MERKLE) { + _validateMerkleRoot(c.merkleRoot); + } else if (mode == VERIFY_SIGNATURE) { + c.merkleRoot = bytes32(0); + c.maxMintablePerAccount = type(uint32).max; + } else if (mode == PLATFORM_AIRDROP) { + c.merkleRoot = bytes32(0); + c.maxMintablePerAccount = type(uint32).max; + c.price = 0; // Platform airdrop mode doesn't have a price. + } else { + revert InvalidMode(); + } + + // If GA, overwrite any immutable variables as required. + if (c.tier == GA_TIER) { + c.endTime = type(uint32).max; + c.maxMintablePerAccount = type(uint32).max; + // We allow the `price` to be the minimum price if the `mode` is `VERIFY_SIGNATURE`. + // Otherwise, the actual default price is the live value of `gaPrice[platform]`, + // and we'll simply set it to zero to avoid a SLOAD. + if (mode != VERIFY_SIGNATURE) c.price = 0; + // Set `maxMintable` to the maximum only if `mode` is `DEFAULT`. + if (mode == DEFAULT) c.maxMintable = type(uint32).max; + } + + _validateTimeRange(c.startTime, c.endTime); + _validateMaxMintablePerAccount(c.maxMintablePerAccount); + _validateMaxMintable(c.maxMintable); + + unchecked { + MintData storage tierHead = _mintData[LibOps.packId(c.edition, c.tier, 0)]; + MintData storage editionHead = _mintData[LibOps.packId(c.edition, 0)]; + + scheduleNum = tierHead.nextScheduleNum; + uint256 n = scheduleNum; + if (++n >= 1 << 8) LibOps.revertOverflow(); + tierHead.nextScheduleNum = uint8(n); + + n = editionHead.numMintData; + if (++n >= 1 << 16) LibOps.revertOverflow(); + editionHead.numMintData = uint16(n); + + uint256 mintId = LibOps.packId(c.edition, c.tier, scheduleNum); + + MintData storage d = _mintData[mintId]; + d.platform = c.platform; + d.price = c.price; + d.startTime = c.startTime; + d.endTime = c.endTime; + d.maxMintablePerAccount = c.maxMintablePerAccount; + d.maxMintable = c.maxMintable; + d.affiliateFeeBPS = c.affiliateFeeBPS; + 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); + } + } + + /** + * @inheritdoc ISuperMinterE + */ + function mintTo(MintTo calldata p) public payable returns (uint256 fromTokenId) { + MintData storage d = _getMintData(LibOps.packId(p.edition, p.tier, p.scheduleNum)); + + /* ------------------- CHECKS AND UPDATES ------------------- */ + + _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); + } + + /* ----------------- COMPUTE AND ACCRUE FEES ---------------- */ + + MintedLogData memory l; + // Blocking same address self referral is left curved, but we do anyway. + l.affiliate = p.to == p.affiliate ? address(0) : p.affiliate; + // Affiliate check. + l.affiliated = _isAffiliatedWithProof(d, l.affiliate, p.affiliateProof); + + TotalPriceAndFees memory f = _totalPriceAndFees(p.tier, d, p.quantity, p.signedPrice, l.affiliated); + + 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; + l.finalAffiliateFee = f.finalAffiliateFee; + + // Platform and affilaite fees are accrued mappings. + // Artist earnings are directly forwarded to the nft contract in mint call below. + // Overflow not possible since all fees are uint128s. + unchecked { + if (l.finalAffiliateFee != 0) { + if (l.erc20 == address(0)) { + affiliateFeesAccrued[p.affiliate] += l.finalAffiliateFee; + } else { + affiliateERC20FeesAccrued[p.affiliate][l.erc20] += l.finalAffiliateFee; + } + } + if (l.finalPlatformFee != 0) { + if (l.erc20 == address(0)) { + platformFeesAccrued[d.platform] += l.finalPlatformFee; + } else { + platformERC20FeesAccrued[d.platform][l.erc20] += l.finalPlatformFee; + } + } + } + + /* ------------------------- MINT --------------------------- */ + + ISoundEditionV2_1 edition = ISoundEditionV2_1(p.edition); + l.quantity = p.quantity; + l.allowlisted = p.allowlisted; + l.allowlistedQuantity = p.allowlistedQuantity; + l.signedClaimTicket = p.signedClaimTicket; + l.requiredPayment = 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; + } + + /** + * @inheritdoc ISuperMinterE + */ + function platformAirdrop(PlatformAirdrop calldata p) public returns (uint256 fromTokenId) { + MintData storage d = _getMintData(LibOps.packId(p.edition, p.tier, p.scheduleNum)); + + /* ------------------- CHECKS AND UPDATES ------------------- */ + + _requireMintOpen(d); + + if (d.mode != PLATFORM_AIRDROP) revert InvalidMode(); + _verifyAndClaimPlatfromAidropSignature(d, p); + + _incrementPlatformAirdropMinted(d, p); + + /* ------------------------- MINT --------------------------- */ + + ISoundEditionV2_1 edition = ISoundEditionV2_1(p.edition); + fromTokenId = edition.airdrop(p.tier, p.to, p.signedQuantity); + + emit PlatformAirdropped(p.edition, p.tier, p.scheduleNum, p.to, p.signedQuantity, fromTokenId); + } + + // Per edition mint parameter setters: + // ----------------------------------- + // These functions can only be called by the owner or admin of the edition. + + /** + * @inheritdoc ISuperMinterE + */ + function setPrice( + address edition, + uint8 tier, + uint8 scheduleNum, + uint128 price + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + // If the tier is GA and the `mode` is `VERIFY_SIGNATURE`, we'll use `gaPrice[platform]`. + if (tier == GA_TIER && d.mode != VERIFY_SIGNATURE) revert NotConfigurable(); + // Platform airdropped mints will not have a price. + if (d.mode == PLATFORM_AIRDROP) revert NotConfigurable(); + d.price = price; + emit PriceSet(edition, tier, scheduleNum, price); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setPaused( + address edition, + uint8 tier, + uint8 scheduleNum, + bool paused + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + d.flags = LibOps.setFlagTo(d.flags, _MINT_PAUSED_FLAG, paused); + emit PausedSet(edition, tier, scheduleNum, paused); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setTimeRange( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 startTime, + uint32 endTime + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + // For GA tier, `endTime` will always be `type(uint32).max`. + if (tier == GA_TIER && endTime != type(uint32).max) revert NotConfigurable(); + _validateTimeRange(startTime, endTime); + d.startTime = startTime; + d.endTime = endTime; + emit TimeRangeSet(edition, tier, scheduleNum, startTime, endTime); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setStartTime( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 startTime + ) public { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + setTimeRange(edition, tier, scheduleNum, startTime, _mintData[mintId].endTime); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setAffiliateFee( + address edition, + uint8 tier, + uint8 scheduleNum, + uint16 bps + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + _validateAffiliateFeeBPS(bps); + d.affiliateFeeBPS = bps; + emit AffiliateFeeSet(edition, tier, scheduleNum, bps); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setAffiliateMerkleRoot( + address edition, + uint8 tier, + uint8 scheduleNum, + bytes32 root + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + d.affiliateMerkleRoot = root; + emit AffiliateMerkleRootSet(edition, tier, scheduleNum, root); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setMaxMintablePerAccount( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 value + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + // GA tier will have `type(uint32).max`. + if (tier == GA_TIER) revert NotConfigurable(); + // Signature mints will have `type(uint32).max`. + if (d.mode == VERIFY_SIGNATURE) revert NotConfigurable(); + // Platform airdrops will have `type(uint32).max`. + if (d.mode == PLATFORM_AIRDROP) revert NotConfigurable(); + _validateMaxMintablePerAccount(value); + d.maxMintablePerAccount = value; + emit MaxMintablePerAccountSet(edition, tier, scheduleNum, value); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setMaxMintable( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 value + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + // We allow edits for GA tier, if the `mode` is not `DEFAULT`. + if (tier == GA_TIER && d.mode == DEFAULT) revert NotConfigurable(); + _validateMaxMintable(value); + d.maxMintable = value; + emit MaxMintableSet(edition, tier, scheduleNum, value); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setMerkleRoot( + address edition, + uint8 tier, + uint8 scheduleNum, + bytes32 merkleRoot + ) public onlyEditionOwnerOrAdmin(edition) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + if (d.mode != VERIFY_MERKLE) revert NotConfigurable(); + _validateMerkleRoot(merkleRoot); + d.merkleRoot = merkleRoot; + emit MerkleRootSet(edition, tier, scheduleNum, merkleRoot); + } + + // Withdrawal functions: + // --------------------- + // These functions can be called by anyone. + + /** + * @inheritdoc ISuperMinterE + */ + function withdrawForAffiliate(address affiliate) public { + uint256 accrued = affiliateFeesAccrued[affiliate]; + if (accrued != 0) { + affiliateFeesAccrued[affiliate] = 0; + SafeTransferLib.forceSafeTransferETH(affiliate, accrued); + emit AffiliateFeesWithdrawn(affiliate, accrued); + } + } + + /** + * @inheritdoc ISuperMinterE + */ + function withdrawERC20ForAffiliate(address affiliate, address erc20) 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 + */ + function withdrawForPlatform(address platform) public { + address recipient = platformFeeAddress[platform]; + _validatePlatformFeeAddress(recipient); + uint256 accrued = platformFeesAccrued[platform]; + if (accrued != 0) { + platformFeesAccrued[platform] = 0; + SafeTransferLib.forceSafeTransferETH(recipient, accrued); + emit PlatformFeesWithdrawn(platform, accrued); + } + } + + /** + * @inheritdoc ISuperMinterE + */ + function withdrawERC20ForPlatform(address platform, address erc20) 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. + + /** + * @inheritdoc ISuperMinterE + */ + function setPlatformFeeAddress(address recipient) public { + address sender = LibMulticaller.senderOrSigner(); + _validatePlatformFeeAddress(recipient); + platformFeeAddress[sender] = recipient; + emit PlatformFeeAddressSet(sender, recipient); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setPlatformFeeConfig(uint8 tier, PlatformFeeConfig memory c) public { + address sender = LibMulticaller.senderOrSigner(); + _validatePlatformFeeConfig(c); + _platformFeeConfigs[LibOps.packId(sender, tier)] = c; + emit PlatformFeeConfigSet(sender, tier, c); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setDefaultPlatformFeeConfig(PlatformFeeConfig memory c) public { + address sender = LibMulticaller.senderOrSigner(); + _validatePlatformFeeConfig(c); + _platformFeeConfigs[LibOps.packId(sender, _DEFAULT_FEE_CONFIG_INDEX)] = c; + emit DefaultPlatformFeeConfigSet(sender, c); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setGAPrice(uint128 price) public { + address sender = LibMulticaller.senderOrSigner(); + gaPrice[sender] = price; + emit GAPriceSet(sender, price); + } + + /** + * @inheritdoc ISuperMinterE + */ + function setPlatformSigner(address signer) public { + address sender = LibMulticaller.senderOrSigner(); + platformSigner[sender] = signer; + emit PlatformSignerSet(sender, signer); + } + + // Misc functions: + // --------------- + + /** + * @dev For calldata compression. + */ + fallback() external payable { + LibZip.cdFallback(); + } + + /** + * @dev For calldata compression. + */ + receive() external payable { + LibZip.cdFallback(); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISuperMinterE + */ + function computeMintToDigest(MintTo calldata p) public view returns (bytes32) { + // prettier-ignore + return + _hashTypedData(keccak256(abi.encode( + MINT_TO_TYPEHASH, + p.edition, + p.tier, + p.scheduleNum, + p.to, + p.signedQuantity, + p.signedClaimTicket, + p.signedPrice, + p.signedDeadline, + p.affiliate + ))); + } + + /** + * @inheritdoc ISuperMinterE + */ + function computePlatformAirdropDigest(PlatformAirdrop calldata p) public view returns (bytes32) { + // prettier-ignore + return + _hashTypedData(keccak256(abi.encode( + PLATFORM_AIRDROP_TYPEHASH, + p.edition, + p.tier, + p.scheduleNum, + keccak256(abi.encodePacked(p.to)), + p.signedQuantity, + p.signedClaimTicket, + p.signedDeadline + ))); + } + + /** + * @inheritdoc ISuperMinterE + */ + function totalPriceAndFees( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 quantity, + bool hasValidAffiliate + ) public view returns (TotalPriceAndFees memory) { + return totalPriceAndFeesWithSignedPrice(edition, tier, scheduleNum, quantity, 0, hasValidAffiliate); + } + + /** + * @inheritdoc ISuperMinterE + */ + function totalPriceAndFeesWithSignedPrice( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 quantity, + uint128 signedPrice, + bool hasValidAffiliate + ) public view returns (TotalPriceAndFees memory) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + return _totalPriceAndFees(tier, _getMintData(mintId), quantity, signedPrice, hasValidAffiliate); + } + + /** + * @inheritdoc ISuperMinterE + */ + function nextScheduleNum(address edition, uint8 tier) public view returns (uint8) { + return _mintData[LibOps.packId(edition, tier, 0)].nextScheduleNum; + } + + /** + * @inheritdoc ISuperMinterE + */ + function numberMinted( + address edition, + uint8 tier, + uint8 scheduleNum, + address collector + ) external view returns (uint32) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + return _numberMinted[collector].get(mintId); + } + + /** + * @inheritdoc ISuperMinterE + */ + function isAffiliatedWithProof( + address edition, + uint8 tier, + uint8 scheduleNum, + address affiliate, + bytes32[] calldata affiliateProof + ) public view virtual returns (bool) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + return _isAffiliatedWithProof(_getMintData(mintId), affiliate, affiliateProof); + } + + /** + * @inheritdoc ISuperMinterE + */ + function isAffiliated( + address edition, + uint8 tier, + uint8 scheduleNum, + address affiliate + ) public view virtual returns (bool) { + return isAffiliatedWithProof(edition, tier, scheduleNum, affiliate, MerkleProofLib.emptyProof()); + } + + /** + * @inheritdoc ISuperMinterE + */ + function checkClaimTickets( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32[] calldata claimTickets + ) public view returns (bool[] memory claimed) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + LibBitmap.Bitmap storage bitmap = _claimsBitmaps[mintId]; + claimed = new bool[](claimTickets.length); + unchecked { + for (uint256 i; i != claimTickets.length; i++) { + claimed[i] = bitmap.get(claimTickets[i]); + } + } + } + + /** + * @inheritdoc ISuperMinterE + */ + function platformFeeConfig(address platform, uint8 tier) public view returns (PlatformFeeConfig memory) { + return _platformFeeConfigs[LibOps.packId(platform, tier)]; + } + + /** + * @inheritdoc ISuperMinterE + */ + function defaultPlatformFeeConfig(address platform) public view returns (PlatformFeeConfig memory) { + return _platformFeeConfigs[LibOps.packId(platform, _DEFAULT_FEE_CONFIG_INDEX)]; + } + + /** + * @inheritdoc ISuperMinterE + */ + function effectivePlatformFeeConfig(address platform, uint8 tier) public view returns (PlatformFeeConfig memory) { + PlatformFeeConfig memory c = _platformFeeConfigs[LibOps.packId(platform, tier)]; + if (!c.active) c = _platformFeeConfigs[LibOps.packId(platform, _DEFAULT_FEE_CONFIG_INDEX)]; + if (!c.active) delete c; // Set all values to zero. + return c; + } + + /** + * @inheritdoc ISuperMinterE + */ + function mintInfoList(address edition) public view returns (MintInfo[] memory a) { + unchecked { + MintData storage editionHead = _mintData[LibOps.packId(edition, 0)]; + uint256 n = editionHead.numMintData; // Linked-list length. + uint16 p = editionHead.head; // Current linked-list pointer. + a = new MintInfo[](n); + // Traverse the linked-list and fill the array in reverse. + // Front: earliest added mint schedule. Back: latest added mint schedule. + while (n != 0) { + MintData storage d = _mintData[LibOps.packId(edition, p)]; + a[--n] = mintInfo(edition, uint8(p >> 8), uint8(p)); + p = d.next; + } + } + } + + /** + * @inheritdoc ISuperMinterE + */ + function mintInfo( + address edition, + uint8 tier, + uint8 scheduleNum + ) public view returns (MintInfo memory info) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + MintData storage d = _getMintData(mintId); + info.edition = edition; + info.tier = tier; + info.scheduleNum = scheduleNum; + info.platform = d.platform; + info.price = tier == GA_TIER && d.mode != VERIFY_SIGNATURE ? gaPrice[d.platform] : d.price; + info.startTime = d.startTime; + info.endTime = d.endTime; + info.maxMintablePerAccount = d.maxMintablePerAccount; + info.maxMintable = d.maxMintable; + info.minted = d.minted; + info.affiliateFeeBPS = d.affiliateFeeBPS; + info.mode = d.mode; + info.paused = _isPaused(d); + info.affiliateMerkleRoot = d.affiliateMerkleRoot; + info.merkleRoot = d.merkleRoot; + info.signer = platformSigner[d.platform]; + } + + /** + * @inheritdoc ISuperMinterE + */ + function name() external pure returns (string memory name_) { + (name_, ) = _domainNameAndVersion(); + } + + /** + * @inheritdoc ISuperMinterE + */ + function version() external pure returns (string memory version_) { + (, version_) = _domainNameAndVersion(); + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + LibOps.or(interfaceId == type(ISuperMinterE).interfaceId, interfaceId == this.supportsInterface.selector); + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + // Validations: + // ------------ + + /** + * @dev Guards a function to make it callable only by the edition's owner or admin. + * @param edition The edition address. + */ + modifier onlyEditionOwnerOrAdmin(address edition) { + _requireOnlyEditionOwnerOrAdmin(edition); + _; + } + + /** + * @dev Requires that the caller is the owner or admin of `edition`. + * @param edition The edition address. + */ + function _requireOnlyEditionOwnerOrAdmin(address edition) internal view { + address sender = LibMulticaller.senderOrSigner(); + if (sender != OwnableRoles(edition).owner()) + if (!OwnableRoles(edition).hasAnyRole(sender, LibOps.ADMIN_ROLE)) LibOps.revertUnauthorized(); + } + + /** + * @dev Validates that `startTime <= endTime`. + * @param startTime The start time of the mint. + * @param endTime The end time of the mint. + */ + function _validateTimeRange(uint32 startTime, uint32 endTime) internal pure { + if (startTime > endTime) revert InvalidTimeRange(); + } + + /** + * @dev Validates that the max mintable amount per account is not zero. + * @param value The max mintable amount. + */ + function _validateMaxMintablePerAccount(uint32 value) internal pure { + if (value == 0) revert MaxMintablePerAccountIsZero(); + } + + /** + * @dev Validates that the max mintable per schedule. + * @param value The max mintable amount. + */ + function _validateMaxMintable(uint32 value) internal pure { + if (value == 0) revert MaxMintableIsZero(); + } + + /** + * @dev Validates that the Merkle root is not empty. + * @param merkleRoot The Merkle root. + */ + function _validateMerkleRoot(bytes32 merkleRoot) internal pure { + if (merkleRoot == bytes32(0)) revert MerkleRootIsEmpty(); + } + + /** + * @dev Validates that the affiliate fee BPS does not exceed the max threshold. + * @param bps The affiliate fee BPS. + */ + function _validateAffiliateFeeBPS(uint16 bps) internal pure { + if (bps > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS(); + } + + /** + * @dev Validates the platform fee configuration. + * @param c The platform fee configuration. + */ + function _validatePlatformFeeConfig(PlatformFeeConfig memory c) internal pure { + if ( + LibOps.or( + LibOps.or( + c.platformTxFlatFee > MAX_PLATFORM_PER_TX_FLAT_FEE, + c.platformMintFeeBPS > MAX_PLATFORM_PER_MINT_FEE_BPS + ), + LibOps.or( + c.artistMintReward > MAX_PER_MINT_REWARD, + c.affiliateMintReward > MAX_PER_MINT_REWARD, + c.platformMintReward > MAX_PER_MINT_REWARD + ), + LibOps.or( + c.thresholdArtistMintReward > MAX_PER_MINT_REWARD, + c.thresholdAffiliateMintReward > MAX_PER_MINT_REWARD, + c.thresholdPlatformMintReward > MAX_PER_MINT_REWARD + ) + ) + ) revert InvalidPlatformFeeConfig(); + } + + /** + * @dev Validates that the platform fee address is not the zero address. + * @param a The platform fee address. + */ + function _validatePlatformFeeAddress(address a) internal pure { + if (a == address(0)) revert PlatformFeeAddressIsZero(); + } + + // EIP-712: + // -------- + + /** + * @dev Override for EIP-712. + * @return name_ The EIP-712 name. + * @return version_ The EIP-712 version. + */ + function _domainNameAndVersion() + internal + pure + virtual + override + returns (string memory name_, string memory version_) + { + name_ = "SuperMinterE"; + version_ = "1"; + } + + // Minting: + // -------- + + /** + * @dev Increments the number minted in the mint and the number minted by the collector. + * @param mode The mint mode. + * @param d The mint data storage pointer. + * @param p The mint-to parameters. + */ + function _incrementMinted( + uint8 mode, + MintData storage d, + MintTo calldata p + ) internal { + unchecked { + // Increment the number minted in the mint. + uint256 n = uint256(d.minted) + uint256(p.quantity); // The next `minted`. + if (n > d.maxMintable) revert ExceedsMintSupply(); + d.minted = uint32(n); + + // Increment the number minted by the collector. + uint256 mintId = LibOps.packId(p.edition, p.tier, p.scheduleNum); + if (mode == VERIFY_MERKLE) { + LibMap.Uint32Map storage m = _numberMinted[p.allowlisted]; + n = uint256(m.get(mintId)) + uint256(p.quantity); + // Check that `n` does not exceed either the default limit, + // or the limit in the Merkle leaf if a non-zero value is provided. + if (LibOps.or(n > d.maxMintablePerAccount, n > p.allowlistedQuantity)) revert ExceedsMaxPerAccount(); + m.set(mintId, uint32(n)); + } else { + LibMap.Uint32Map storage m = _numberMinted[p.to]; + n = uint256(m.get(mintId)) + uint256(p.quantity); + if (n > d.maxMintablePerAccount) revert ExceedsMaxPerAccount(); + m.set(mintId, uint32(n)); + } + } + } + + /** + * @dev Increments the number minted in the mint and the number minted by the collector. + * @param d The mint data storage pointer. + * @param p The platform airdrop parameters. + */ + function _incrementPlatformAirdropMinted(MintData storage d, PlatformAirdrop calldata p) internal { + unchecked { + uint256 mintId = LibOps.packId(p.edition, p.tier, p.scheduleNum); + uint256 toLength = p.to.length; + + // Increment the number minted in the mint. + uint256 n = uint256(d.minted) + toLength * uint256(p.signedQuantity); // The next `minted`. + if (n > d.maxMintable) revert ExceedsMintSupply(); + d.minted = uint32(n); + + // Increment the number minted by the collectors. + for (uint256 i; i != toLength; ++i) { + LibMap.Uint32Map storage m = _numberMinted[p.to[i]]; + m.set(mintId, uint32(uint256(m.get(mintId)) + uint256(p.signedQuantity))); + } + } + } + + /** + * @dev Requires that the mint is open and not paused. + * @param d The mint data storage pointer. + */ + function _requireMintOpen(MintData storage d) internal view { + if (LibOps.or(block.timestamp < d.startTime, block.timestamp > d.endTime)) + revert MintNotOpen(block.timestamp, d.startTime, d.endTime); + if (_isPaused(d)) revert MintPaused(); // Check if the mint is not paused. + } + + /** + * @dev Verify the signature, and mark the signed claim ticket as claimed. + * @param d The mint data storage pointer. + * @param p The mint-to parameters. + */ + function _verifyAndClaimSignature(MintData storage d, MintTo calldata p) internal { + if (p.quantity > p.signedQuantity) revert ExceedsSignedQuantity(); + address signer = platformSigner[d.platform]; + if (!SignatureCheckerLib.isValidSignatureNowCalldata(signer, computeMintToDigest(p), p.signature)) + revert InvalidSignature(); + if (block.timestamp > p.signedDeadline) revert SignatureExpired(); + uint256 mintId = LibOps.packId(p.edition, p.tier, p.scheduleNum); + if (!_claimsBitmaps[mintId].toggle(p.signedClaimTicket)) revert SignatureAlreadyUsed(); + } + + /** + * @dev Verify the platform airdrop signature, and mark the signed claim ticket as claimed. + * @param d The mint data storage pointer. + * @param p The platform airdrop parameters. + */ + function _verifyAndClaimPlatfromAidropSignature(MintData storage d, PlatformAirdrop calldata p) internal { + // Unlike regular signature mints, platform airdrops only use `signedQuantity`. + address signer = platformSigner[d.platform]; + if (!SignatureCheckerLib.isValidSignatureNowCalldata(signer, computePlatformAirdropDigest(p), p.signature)) + revert InvalidSignature(); + if (block.timestamp > p.signedDeadline) revert SignatureExpired(); + uint256 mintId = LibOps.packId(p.edition, p.tier, p.scheduleNum); + if (!_claimsBitmaps[mintId].toggle(p.signedClaimTicket)) revert SignatureAlreadyUsed(); + } + + /** + * @dev Verify the Merkle proof. + * @param d The mint data storage pointer. + * @param p The mint-to parameters. + */ + function _verifyMerkle(MintData storage d, MintTo calldata p) internal view { + uint32 allowlistedQuantity = p.allowlistedQuantity; + address allowlisted = p.allowlisted; + // Revert if `allowlisted` is the zero address to prevent libraries + // that fill up partial Merkle trees with empty leafs from screwing things up. + if (allowlisted == address(0)) revert InvalidMerkleProof(); + // If `allowlistedQuantity` is the max limit, we've got to check two cases for backwards compatibility. + if (allowlistedQuantity == type(uint32).max) { + // Revert if neither `keccak256(abi.encodePacked(allowlisted))` nor + // `keccak256(abi.encodePacked(allowlisted, uint32(0)))` are in the Merkle tree. + if ( + !p.allowlistProof.verifyCalldata(d.merkleRoot, _leaf(allowlisted)) && + !p.allowlistProof.verifyCalldata(d.merkleRoot, _leaf(allowlisted, type(uint32).max)) + ) revert InvalidMerkleProof(); + } else { + // Revert if `keccak256(abi.encodePacked(allowlisted, uint32(allowlistedQuantity)))` + // is not in the Merkle tree. + if (!p.allowlistProof.verifyCalldata(d.merkleRoot, _leaf(allowlisted, allowlistedQuantity))) + revert InvalidMerkleProof(); + } + // To mint, either the sender or `to` must be equal to `allowlisted`, + address sender = LibMulticaller.senderOrSigner(); + if (!LibOps.or(sender == allowlisted, p.to == allowlisted)) { + // or the sender must be a delegate of `allowlisted`. + if (!DelegateCashLib.checkDelegateForAll(sender, allowlisted)) revert CallerNotDelegated(); + } + } + + /** + * @dev Returns the total price and fees for the mint. + * @param tier The tier. + * @param d The mint data storage pointer. + * @param quantity How many tokens to mint. + * @param signedPrice The signed price. Only for `VERIFY_SIGNATURE`. + * @return f A struct containing the total price and fees. + */ + function _totalPriceAndFees( + uint8 tier, + MintData storage d, + uint32 quantity, + uint128 signedPrice, + bool hasValidAffiliate + ) internal view returns (TotalPriceAndFees memory f) { + // All flat prices are stored as uint128s in storage. + // The quantity is a uint32. Multiplications between a uint128 and uint32 won't overflow. + unchecked { + PlatformFeeConfig memory c = effectivePlatformFeeConfig(d.platform, tier); + + // For signature mints, even if it is GA tier, we will use the signed price. + if (d.mode == VERIFY_SIGNATURE) { + if (signedPrice < d.price) revert SignedPriceTooLow(); // Enforce the price floor. + f.unitPrice = signedPrice; + } else if (tier == GA_TIER) { + f.unitPrice = gaPrice[d.platform]; // Else if GA tier, use `gaPrice[platform]`. + } else { + f.unitPrice = d.price; // Else, use the `price`. + } + + // The total price before any additive fees. + f.subTotal = f.unitPrice * uint256(quantity); + + // Artist earns `subTotal` minus any basis points (BPS) split with affiliates and platform + f.finalArtistFee = f.subTotal; + + // `affiliateBPSFee` is deducted from the `finalArtistFee`. + if (d.affiliateFeeBPS != 0 && hasValidAffiliate) { + uint256 affiliateBPSFee = LibOps.rawMulDiv(f.subTotal, d.affiliateFeeBPS, BPS_DENOMINATOR); + f.finalArtistFee -= affiliateBPSFee; + f.finalAffiliateFee = affiliateBPSFee; + } + // `platformBPSFee` is deducted from the `finalArtistFee`. + if (c.platformMintFeeBPS != 0) { + uint256 platformBPSFee = LibOps.rawMulDiv(f.subTotal, c.platformMintFeeBPS, BPS_DENOMINATOR); + f.finalArtistFee -= platformBPSFee; + f.finalPlatformFee = platformBPSFee; + } + + // Protocol rewards are additive to `unitPrice` and paid by the buyer. + // There are 2 sets of rewards, one for prices below `thresholdPrice` and one for prices above. + if (f.unitPrice <= c.thresholdPrice) { + f.finalArtistFee += c.artistMintReward * uint256(quantity); + f.finalPlatformFee += c.platformMintReward * uint256(quantity); + + // The platform is the affiliate if no affiliate is provided. + if (hasValidAffiliate) { + f.finalAffiliateFee += c.affiliateMintReward * uint256(quantity); + } else { + f.finalPlatformFee += c.affiliateMintReward * uint256(quantity); + } + } else { + f.finalArtistFee += c.thresholdArtistMintReward * uint256(quantity); + f.finalPlatformFee += c.thresholdPlatformMintReward * uint256(quantity); + + // The platform is the affiliate if no affiliate is provided + if (hasValidAffiliate) { + f.finalAffiliateFee += c.thresholdAffiliateMintReward * uint256(quantity); + } else { + f.finalPlatformFee += c.thresholdAffiliateMintReward * uint256(quantity); + } + } + + // Per-transaction flat fee. + f.finalPlatformFee += c.platformTxFlatFee; + + // The total is the final value which the minter has to pay. It includes all fees. + f.total = f.finalArtistFee + f.finalAffiliateFee + f.finalPlatformFee; + } + } + + /** + * @dev Returns whether the affiliate is affiliated for the mint + * @param d The mint data storage pointer. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof for the affiliate. + * @return The result. + */ + function _isAffiliatedWithProof( + MintData storage d, + address affiliate, + bytes32[] calldata affiliateProof + ) internal view virtual returns (bool) { + bytes32 root = d.affiliateMerkleRoot; + // If the root is empty, then use the default logic. + if (root == bytes32(0)) return affiliate != address(0); + // Otherwise, check if the affiliate is in the Merkle tree. + // The check that that affiliate is not a zero address is to prevent libraries + // that fill up partial Merkle trees with empty leafs from screwing things up. + return LibOps.and(affiliate != address(0), affiliateProof.verifyCalldata(root, _leaf(affiliate))); + } + + // Utilities: + // ---------- + + /** + * @dev Equivalent to `keccak256(abi.encodePacked(allowlisted))`. + * @param allowlisted The allowlisted address. + * @return result The leaf in the Merkle tree. + */ + function _leaf(address allowlisted) internal pure returns (bytes32 result) { + assembly { + mstore(0x00, allowlisted) + result := keccak256(0x0c, 0x14) + } + } + + /** + * @dev Equivalent to `keccak256(abi.encodePacked(allowlisted, allowlistedQuantity))`. + * @param allowlisted The allowlisted address. + * @param allowlistedQuantity Number of mints allowlisted. + * @return result The leaf in the Merkle tree. + */ + function _leaf(address allowlisted, uint32 allowlistedQuantity) internal pure returns (bytes32 result) { + assembly { + mstore(0x04, allowlistedQuantity) + mstore(0x00, allowlisted) + result := keccak256(0x0c, 0x18) + } + } + + /** + * @dev Retrieves the mint data from storage, reverting if the mint does not exist. + * @param mintId The mint ID. + * @return d The storage pointer to the mint data. + */ + function _getMintData(uint256 mintId) internal view returns (MintData storage d) { + d = _mintData[mintId]; + if (d.flags & _MINT_CREATED_FLAG == 0) revert MintDoesNotExist(); + } + + /** + * @dev Returns whether the mint is paused. + * @param d The storage pointer to the mint data. + * @return Whether the mint is paused. + */ + function _isPaused(MintData storage d) internal view returns (bool) { + return d.flags & _MINT_PAUSED_FLAG != 0; + } +} diff --git a/contracts/modules/interfaces/ISuperMinterE.sol b/contracts/modules/interfaces/ISuperMinterE.sol new file mode 100644 index 00000000..6df575c1 --- /dev/null +++ b/contracts/modules/interfaces/ISuperMinterE.sol @@ -0,0 +1,1067 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; + +/** + * @title ISuperMinterE + * @notice The interface for the generalized minter. + */ +interface ISuperMinterE is IERC165 { + // ============================================================= + // STRUCTS + // ============================================================= + + /** + * @dev A struct containing the arguments to create a mint. + */ + struct MintCreation { + // The edition address. + address edition; + // The base price per token. + // For `VERIFY_SIGNATURE`, this will be the minimum limit of the signed price. + // Will be 0 if the `tier` is `GA_TIER`. + uint128 price; + // The start time of the mint. + uint32 startTime; + // The end time of the mint. + uint32 endTime; + // The maximum number of tokens an account can mint in this mint. + uint32 maxMintablePerAccount; + // The maximum number of tokens mintable. + uint32 maxMintable; + // The affiliate fee BPS. + uint16 affiliateFeeBPS; + // The affiliate Merkle root, if any. + bytes32 affiliateMerkleRoot; + // The tier of the mint. + uint8 tier; + // The address of the platform. + address platform; + // The mode of the mint. Options: `DEFAULT`, `VERIFY_MERKLE`, `VERIFY_SIGNATURE`. + uint8 mode; + // The Merkle root hash, required if `mode` is `VERIFY_MERKLE`. + bytes32 merkleRoot; + // The ERC20 token to denominate the fees in. + address erc20; + } + + /** + * @dev A struct containing the arguments for mint-to. + */ + struct MintTo { + // The mint ID. + address edition; + // The tier of the mint. + uint8 tier; + // The edition-tier schedule number. + uint8 scheduleNum; + // The address to mint to. + address to; + // The number of tokens to mint. + uint32 quantity; + // The allowlisted address. Used if `mode` is `VERIFY_MERKLE`. + address allowlisted; + // The allowlisted quantity. Used if `mode` is `VERIFY_MERKLE`. + // A default zero value means no limit. + uint32 allowlistedQuantity; + // The allowlist Merkle proof. + bytes32[] allowlistProof; + // The signed price. Used if `mode` is `VERIFY_SIGNATURE`. + uint128 signedPrice; + // The signed quantity. Used if `mode` is `VERIFY_SIGNATURE`. + uint32 signedQuantity; + // The signed claimed ticket. Used if `mode` is `VERIFY_SIGNATURE`. + uint32 signedClaimTicket; + // The expiry timestamp for the signature. Used if `mode` is `VERIFY_SIGNATURE`. + uint32 signedDeadline; + // The signature by the signer. Used if `mode` is `VERIFY_SIGNATURE`. + bytes signature; + // The affiliate address. Optional. + address affiliate; + // The Merkle proof for the affiliate. + bytes32[] affiliateProof; + // The attribution ID, optional. + uint256 attributionId; + } + + /** + * @dev A struct containing the arguments for platformAirdrop. + */ + struct PlatformAirdrop { + // The mint ID. + address edition; + // The tier of the mint. + uint8 tier; + // The edition-tier schedule number. + uint8 scheduleNum; + // The addresses to mint to. + address[] to; + // The signed quantity. + uint32 signedQuantity; + // The signed claimed ticket. Used if `mode` is `VERIFY_SIGNATURE`. + uint32 signedClaimTicket; + // The expiry timestamp for the signature. Used if `mode` is `VERIFY_SIGNATURE`. + uint32 signedDeadline; + // The signature by the signer. Used if `mode` is `VERIFY_SIGNATURE`. + bytes signature; + } + + /** + * @dev A struct containing the total prices and fees. + */ + struct TotalPriceAndFees { + // The required Ether value. + // (`subTotal + platformTxFlatFee + artistReward + affiliateReward + platformReward`). + uint256 total; + // The total price before any additive fees. + uint256 subTotal; + // The price per token. + uint256 unitPrice; + // The final artist fee (inclusive of `finalArtistReward`). + uint256 finalArtistFee; + // The total affiliate fee (inclusive of `finalAffiliateReward`). + uint256 finalAffiliateFee; + // The final platform fee + // (inclusive of `finalPlatformReward`, `perTxFlat`, sum of `perMintBPS`). + uint256 finalPlatformFee; + } + + /** + * @dev A struct containing the log data for the `Minted` event. + */ + struct MintedLogData { + // The number of tokens minted. + uint32 quantity; + // The starting token ID minted. + uint256 fromTokenId; + // The allowlisted address. + address allowlisted; + // The allowlisted quantity. + uint32 allowlistedQuantity; + // The signed quantity. + uint32 signedQuantity; + // The signed claim ticket. + uint32 signedClaimTicket; + // The affiliate address. + address affiliate; + // Whether the affiliate address is affiliated. + bool affiliated; + // The total price paid, inclusive of all fees. + uint256 requiredPayment; + // The price per token. + uint256 unitPrice; + // The final artist fee (inclusive of `finalArtistReward`). + uint256 finalArtistFee; + // The total affiliate fee (inclusive of `finalAffiliateReward`). + uint256 finalAffiliateFee; + // The final platform fee + // (inclusive of `finalPlatformReward`, `perTxFlat`, sum of `perMintBPS`). + uint256 finalPlatformFee; + // The erc20 token for payment, if any. + address erc20; + } + + /** + * @dev A struct to hold the fee configuration for a platform and a tier. + */ + struct PlatformFeeConfig { + // The amount of reward to give to the artist per mint. + uint128 artistMintReward; + // The amount of reward to give to the affiliate per mint. + uint128 affiliateMintReward; + // The amount of reward to give to the platform per mint. + uint128 platformMintReward; + // If the price is greater than this, the rewards will become the threshold variants. + uint128 thresholdPrice; + // The amount of reward to give to the artist (`unitPrice >= thresholdPrice`). + uint128 thresholdArtistMintReward; + // The amount of reward to give to the affiliate (`unitPrice >= thresholdPrice`). + uint128 thresholdAffiliateMintReward; + // The amount of reward to give to the platform (`unitPrice >= thresholdPrice`). + uint128 thresholdPlatformMintReward; + // The per-transaction flat fee. + uint128 platformTxFlatFee; + // The per-token fee BPS. + uint16 platformMintFeeBPS; + // Whether the fees are active. + bool active; + } + + /** + * @dev A struct containing the mint information. + */ + struct MintInfo { + // The mint ID. + address edition; + // The tier of the mint. + uint8 tier; + // The edition-tier schedule number. + uint8 scheduleNum; + // The platform address. + address platform; + // The base price per token. + // For `VERIFY_SIGNATURE` this will be the minimum limit of the signed price. + // If the `tier` is `GA_TIER`, and the `mode` is NOT `VERIFY_SIGNATURE`, + // this value will be the GA price instead. + uint128 price; + // The start time of the mint. + uint32 startTime; + // The end time of the mint. + uint32 endTime; + // The maximum number of tokens an account can mint in this mint. + uint32 maxMintablePerAccount; + // The maximum number of tokens mintable. + uint32 maxMintable; + // The total number of tokens minted. + uint32 minted; + // The affiliate fee BPS. + uint16 affiliateFeeBPS; + // The mode of the mint. + uint8 mode; + // Whether the mint is paused. + bool paused; + // Whether the mint already has mints. + bool hasMints; + // The affiliate Merkle root, if any. + bytes32 affiliateMerkleRoot; + // The Merkle root hash, required if `mode` is `VERIFY_MERKLE`. + bytes32 merkleRoot; + // The signer address, used if `mode` is `VERIFY_SIGNATURE` or `PLATFORM_AIRDROP`. + address signer; + } + + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when a new mint is created. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param creation The mint creation struct. + */ + event MintCreated(address indexed edition, uint8 tier, uint8 scheduleNum, MintCreation creation); + + /** + * @dev Emitted when a mint is paused or un-paused. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param paused Whether the mint is paused. + */ + event PausedSet(address indexed edition, uint8 tier, uint8 scheduleNum, bool paused); + + /** + * @dev Emitted when the time range of a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param startTime The start time. + * @param endTime The end time. + */ + event TimeRangeSet(address indexed edition, uint8 tier, uint8 scheduleNum, uint32 startTime, uint32 endTime); + + /** + * @dev Emitted when the base per-token price of a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param price The base per-token price. + */ + event PriceSet(address indexed edition, uint8 tier, uint8 scheduleNum, uint128 price); + + /** + * @dev Emitted when the max mintable per account for a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param value The max mintable per account. + */ + event MaxMintablePerAccountSet(address indexed edition, uint8 tier, uint8 scheduleNum, uint32 value); + + /** + * @dev Emitted when the max mintable for a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param value The max mintable for the mint. + */ + event MaxMintableSet(address indexed edition, uint8 tier, uint8 scheduleNum, uint32 value); + + /** + * @dev Emitted when the Merkle root of a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param merkleRoot The Merkle root of the mint. + */ + event MerkleRootSet(address indexed edition, uint8 tier, uint8 scheduleNum, bytes32 merkleRoot); + + /** + * @dev Emitted when the affiliate fee BPS for a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param bps The affiliate fee BPS. + */ + event AffiliateFeeSet(address indexed edition, uint8 tier, uint8 scheduleNum, uint16 bps); + + /** + * @dev Emitted when the affiliate Merkle root for a mint is updated. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param root The affiliate Merkle root hash. + */ + event AffiliateMerkleRootSet(address indexed edition, uint8 tier, uint8 scheduleNum, bytes32 root); + + /** + * @dev Emitted when tokens are minted. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param to The recipient of the tokens minted. + * @param data The mint-to log data. + * @param attributionId The optional attribution ID. + */ + event Minted( + address indexed edition, + uint8 tier, + uint8 scheduleNum, + address indexed to, + MintedLogData data, + uint256 indexed attributionId + ); + + /** + * @dev Emitted when tokens are platform airdropped. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param to The recipients of the tokens minted. + * @param signedQuantity The amount of tokens per address. + * @param fromTokenId The first token ID minted. + */ + event PlatformAirdropped( + address indexed edition, + uint8 tier, + uint8 scheduleNum, + address[] to, + uint32 signedQuantity, + uint256 fromTokenId + ); + + /** + * @dev Emitted when the platform fee configuration for `tier` is updated. + * @param platform The platform address. + * @param tier The tier of the mint. + * @param config The platform fee configuration. + */ + event PlatformFeeConfigSet(address indexed platform, uint8 tier, PlatformFeeConfig config); + + /** + * @dev Emitted when the default platform fee configuration is updated. + * @param platform The platform address. + * @param config The platform fee configuration. + */ + event DefaultPlatformFeeConfigSet(address indexed platform, PlatformFeeConfig config); + + /** + * @dev Emitted when affiliate fees are withdrawn. + * @param affiliate The recipient of the fees. + * @param accrued The amount of Ether accrued and withdrawn. + */ + 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. + * @param accrued The amount of Ether accrued and withdrawn. + */ + 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. + * @param recipient The platform fee recipient address. + */ + event PlatformFeeAddressSet(address indexed platform, address recipient); + + /** + * @dev Emitted when the per-token price for the GA tier is set. + * @param platform The platform address. + * @param price The price per token for the GA tier. + */ + event GAPriceSet(address indexed platform, uint128 price); + + /** + * @dev Emitted when the signer for a platform is set. + * @param platform The platform address. + * @param signer The signer for the platform. + */ + event PlatformSignerSet(address indexed platform, address signer); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev Exact payment required. + * @param paid The amount of Ether paid. + * @param required The amount of Ether required. + */ + error WrongPayment(uint256 paid, uint256 required); + + /** + * @dev The mint is not opened. + * @param blockTimestamp The current block timestamp. + * @param startTime The opening time of the mint. + * @param endTime The closing time of the mint. + */ + error MintNotOpen(uint256 blockTimestamp, uint32 startTime, uint32 endTime); + + /** + * @dev The mint is paused. + */ + error MintPaused(); + + /** + * @dev Cannot perform the operation when any mints exist. + */ + error MintsAlreadyExist(); + + /** + * @dev The time range is invalid. + */ + error InvalidTimeRange(); + + /** + * @dev The max mintable range is invalid. + */ + error InvalidMaxMintableRange(); + + /** + * @dev The affiliate fee BPS cannot exceed the limit. + */ + error InvalidAffiliateFeeBPS(); + + /** + * @dev The affiliate fee BPS cannot exceed the limit. + */ + error InvalidPlatformFeeBPS(); + + /** + * @dev The affiliate fee BPS cannot exceed the limit. + */ + error InvalidPlatformFlatFee(); + + /** + * @dev Cannot mint more than the maximum limit per account. + */ + error ExceedsMaxPerAccount(); + + /** + * @dev Cannot mint more than the maximum supply. + */ + error ExceedsMintSupply(); + + /** + * @dev Cannot mint more than the signed quantity. + */ + error ExceedsSignedQuantity(); + + /** + * @dev The signature is invalid. + */ + error InvalidSignature(); + + /** + * @dev The signature has expired. + */ + error SignatureExpired(); + + /** + * @dev The signature claim ticket has already been used. + */ + error SignatureAlreadyUsed(); + + /** + * @dev The Merkle root cannot be empty. + */ + error MerkleRootIsEmpty(); + + /** + * @dev The Merkle proof is invalid. + */ + error InvalidMerkleProof(); + + /** + * @dev The caller has not been delegated via delegate cash. + */ + error CallerNotDelegated(); + + /** + * @dev The max mintable amount per account cannot be zero. + */ + error MaxMintablePerAccountIsZero(); + + /** + * @dev The max mintable value cannot be zero. + */ + error MaxMintableIsZero(); + + /** + * @dev The plaform fee address cannot be the zero address. + */ + error PlatformFeeAddressIsZero(); + + /** + * @dev The mint does not exist. + */ + error MintDoesNotExist(); + + /** + * @dev The affiliate provided is invalid. + */ + error InvalidAffiliate(); + + /** + * @dev The mint mode provided is invalid. + */ + error InvalidMode(); + + /** + * @dev The signed price is too low. + */ + error SignedPriceTooLow(); + + /** + * @dev The platform fee configuration provided is invalid. + */ + error InvalidPlatformFeeConfig(); + + /** + * @dev The parameter cannot be configured. + */ + error NotConfigurable(); + + /** + * @dev The ERC20 contract does not exist. + */ + error ERC20DoesNotExist(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Creates a mint. + * @param c The mint creation struct. + * @return scheduleNum The mint ID. + */ + function createEditionMint(MintCreation calldata c) external returns (uint8 scheduleNum); + + /** + * @dev Performs a mint. + * @param p The mint-to parameters. + * @return fromTokenId The first token ID minted. + */ + function mintTo(MintTo calldata p) external payable returns (uint256 fromTokenId); + + /** + * @dev Performs a platform airdrop. + * @param p The platform airdrop parameters. + * @return fromTokenId The first token ID minted. + */ + function platformAirdrop(PlatformAirdrop calldata p) external returns (uint256 fromTokenId); + + /** + * @dev Sets the price of the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param price The price per token. + */ + function setPrice( + address edition, + uint8 tier, + uint8 scheduleNum, + uint128 price + ) external; + + /** + * @dev Pause or unpase the the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param paused Whether to pause the mint. + */ + function setPaused( + address edition, + uint8 tier, + uint8 scheduleNum, + bool paused + ) external; + + /** + * @dev Sets the time range for the the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param startTime The mint start time. + * @param endTime The mint end time. + */ + function setTimeRange( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 startTime, + uint32 endTime + ) external; + + /** + * @dev Sets the start time for the the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param startTime The mint start time. + */ + function setStartTime( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 startTime + ) external; + + /** + * @dev Sets the affiliate fee BPS for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param bps The fee BPS. + */ + function setAffiliateFee( + address edition, + uint8 tier, + uint8 scheduleNum, + uint16 bps + ) external; + + /** + * @dev Sets the affiliate Merkle root for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param root The affiliate Merkle root. + */ + function setAffiliateMerkleRoot( + address edition, + uint8 tier, + uint8 scheduleNum, + bytes32 root + ) external; + + /** + * @dev Sets the max mintable per account. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param value The max mintable per account. + */ + function setMaxMintablePerAccount( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 value + ) external; + + /** + * @dev Sets the max mintable for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param value The max mintable for the mint. + */ + function setMaxMintable( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 value + ) external; + + /** + * @dev Sets the mode for the mint. The mint mode must be `VERIFY_MERKLE`. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param merkleRoot The Merkle root of the mint. + */ + function setMerkleRoot( + address edition, + uint8 tier, + uint8 scheduleNum, + bytes32 merkleRoot + ) external; + + /** + * @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 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 affiliate The affiliate address. + * @param erc20 The erc20 token address. + */ + function withdrawERC20ForAffiliate(address affiliate, address erc20) external; + + /** + * @dev Withdraws all accrued ERC20 fees of the platform, to the their fee address. + * @param platform The platform address. + * @param erc20 The erc20 token address. + */ + function withdrawERC20ForPlatform(address platform, address erc20) external; + + /** + * @dev Allows the caller, as a platform, to set their fee address + * @param recipient The platform fee address of the caller. + */ + function setPlatformFeeAddress(address recipient) external; + + /** + * @dev Allows the caller, as a platform, to set their per-tier fee configuration. + * @param tier The tier of the mint. + * @param c The platform fee configuration struct. + */ + function setPlatformFeeConfig(uint8 tier, PlatformFeeConfig memory c) external; + + /** + * @dev Allows the caller, as a platform, to set their default fee configuration. + * @param c The platform fee configuration struct. + */ + function setDefaultPlatformFeeConfig(PlatformFeeConfig memory c) external; + + /** + * @dev Allows the platform to set the price for the GA tier. + * @param price The price per token for the GA tier. + */ + function setGAPrice(uint128 price) external; + + /** + * @dev Allows the platform to set their signer. + * @param signer The signer for the platform. + */ + function setPlatformSigner(address signer) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the GA tier. Which is 0. + * @return The constant value. + */ + function GA_TIER() external pure returns (uint8); + + /** + * @dev The EIP-712 typehash for signed mints. + * @return The constant value. + */ + function MINT_TO_TYPEHASH() external pure returns (bytes32); + + /** + * @dev The EIP-712 typehash for platform airdrop mints. + * @return The constant value. + */ + function PLATFORM_AIRDROP_TYPEHASH() external pure returns (bytes32); + + /** + * @dev The default mint mode. + * @return The constant value. + */ + function DEFAULT() external pure returns (uint8); + + /** + * @dev The mint mode for Merkle drops. + * @return The constant value. + */ + function VERIFY_MERKLE() external pure returns (uint8); + + /** + * @dev The mint mode for Merkle drops. + * @return The constant value. + */ + function VERIFY_SIGNATURE() external pure returns (uint8); + + /** + * @dev The mint mode for platform airdrop. + * @return The constant value. + */ + function PLATFORM_AIRDROP() external pure returns (uint8); + + /** + * @dev The denominator used in BPS fee calculations. + * @return The constant value. + */ + function BPS_DENOMINATOR() external pure returns (uint16); + + /** + * @dev The maximum affiliate fee BPS. + * @return The constant value. + */ + function MAX_AFFILIATE_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum per-mint platform fee BPS. + * @return The constant value. + */ + function MAX_PLATFORM_PER_MINT_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum per-mint reward. Applies to artists, affiliates, platform. + * @return The constant value. + */ + function MAX_PER_MINT_REWARD() external pure returns (uint128); + + /** + * @dev The maximum platform per-transaction flat fee. + * @return The constant value. + */ + function MAX_PLATFORM_PER_TX_FLAT_FEE() external pure returns (uint128); + + /** + * @dev Returns the amount of fees accrued by the platform. + * @param platform The platform address. + * @return The latest value. + */ + function platformFeesAccrued(address platform) external view returns (uint256); + + /** + * @dev Returns the fee recipient for the platform. + * @param platform The platform address. + * @return The configured value. + */ + function platformFeeAddress(address platform) external view returns (address); + + /** + * @dev Returns the amount of fees accrued by the affiliate. + * @param affiliate The affiliate address. + * @return The latest value. + */ + function affiliateFeesAccrued(address affiliate) external view returns (uint256); + + /** + * @dev Returns the EIP-712 digest of the mint-to data for signature mints. + * @param p The mint-to parameters. + * @return The computed value. + */ + function computeMintToDigest(MintTo calldata p) external view returns (bytes32); + + /** + * @dev Returns the EIP-712 digest of the mint-to data for platform airdrops. + * @param p The platform airdrop parameters. + * @return The computed value. + */ + function computePlatformAirdropDigest(PlatformAirdrop calldata p) external view returns (bytes32); + + /** + * @dev Returns the total price and fees for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param quantity How many tokens to mint. + * @param hasValidAffiliate Whether there is a valid affiliate for the mint. + * @return A struct containing the total price and fees. + */ + function totalPriceAndFees( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 quantity, + bool hasValidAffiliate + ) external view returns (TotalPriceAndFees memory); + + /** + * @dev Returns the total price and fees for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param quantity How many tokens to mint. + * @param signedPrice The signed price. + * @param hasValidAffiliate Whether there is a valid affiliate for the mint. + * @return A struct containing the total price and fees. + */ + function totalPriceAndFeesWithSignedPrice( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 quantity, + uint128 signedPrice, + bool hasValidAffiliate + ) external view returns (TotalPriceAndFees memory); + + /** + * @dev Returns the GA price for the platform. + * @param platform The platform address. + * @return The configured value. + */ + function gaPrice(address platform) external view returns (uint128); + + /** + * @dev Returns the signer for the platform. + * @param platform The platform address. + * @return The configured value. + */ + function platformSigner(address platform) external view returns (address); + + /** + * @dev Returns the next mint schedule number for the edition-tier. + * @param edition The Sound Edition address. + * @param tier The tier. + * @return The next schedule number for the edition-tier. + */ + function nextScheduleNum(address edition, uint8 tier) external view returns (uint8); + + /** + * @dev Returns the number of tokens minted by `collector` for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param collector The address which tokens are minted to, + * or in the case of `VERIFY_MERKLE`, is the allowlisted address. + * @return The number of tokens minted. + */ + function numberMinted( + address edition, + uint8 tier, + uint8 scheduleNum, + address collector + ) external view returns (uint32); + + /** + * @dev Returns whether the affiliate is affiliated for the mint + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof for the affiliate. + * @return The result. + */ + function isAffiliatedWithProof( + address edition, + uint8 tier, + uint8 scheduleNum, + address affiliate, + bytes32[] calldata affiliateProof + ) external view returns (bool); + + /** + * @dev Returns whether the affiliate is affiliated for the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param affiliate The affiliate address. + * @return A boolean on whether the affiliate is affiliated for the mint. + */ + function isAffiliated( + address edition, + uint8 tier, + uint8 scheduleNum, + address affiliate + ) external view returns (bool); + + /** + * @dev Returns whether the claim tickets have been used. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @param claimTickets An array of claim tickets. + * @return An array of bools, where true means that a ticket has been used. + */ + function checkClaimTickets( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32[] calldata claimTickets + ) external view returns (bool[] memory); + + /** + * @dev Returns the platform fee configuration for the tier. + * @param platform The platform address. + * @param tier The tier of the mint. + * @return The platform fee configuration struct. + */ + function platformFeeConfig(address platform, uint8 tier) external view returns (PlatformFeeConfig memory); + + /** + * @dev Returns the default platform fee configuration. + * @param platform The platform address. + * @return The platform fee configuration struct. + */ + function defaultPlatformFeeConfig(address platform) external view returns (PlatformFeeConfig memory); + + /** + * @dev Returns the effective platform fee configuration. + * @param platform The platform address. + * @param tier The tier of the mint. + * @return The platform fee configuration struct. + */ + function effectivePlatformFeeConfig(address platform, uint8 tier) external view returns (PlatformFeeConfig memory); + + /** + * @dev Returns an array of mint information structs pertaining to the mint. + * @param edition The Sound Edition address. + * @return An array of mint information structs. + */ + function mintInfoList(address edition) external view returns (MintInfo[] memory); + + /** + * @dev Returns information pertaining to the mint. + * @param edition The address of the Sound Edition. + * @param tier The tier. + * @param scheduleNum The edition-tier schedule number. + * @return The mint info struct. + */ + function mintInfo( + address edition, + uint8 tier, + uint8 scheduleNum + ) external view returns (MintInfo memory); + + /** + * @dev Retuns the EIP-712 name for the contract. + * @return The constant value. + */ + function name() external pure returns (string memory); + + /** + * @dev Retuns the EIP-712 version for the contract. + * @return The constant value. + */ + function version() external pure returns (string memory); +} diff --git a/lib/solady b/lib/solady index cde0a5fb..9de1fe26 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit cde0a5fb594da8655ba6bfcdc2e40a7c870c0cc0 +Subproject commit 9de1fe26af4f4b1bbb4b5efcedc503342fc55ee8 diff --git a/tests/modules/SuperMinterE.t.sol b/tests/modules/SuperMinterE.t.sol new file mode 100644 index 00000000..dc68b4d2 --- /dev/null +++ b/tests/modules/SuperMinterE.t.sol @@ -0,0 +1,1055 @@ +pragma solidity ^0.8.16; + +import { Merkle } from "murky/Merkle.sol"; +import { IERC721AUpgradeable, ISoundEditionV2_1, SoundEditionV2_1 } from "@core/SoundEditionV2_1.sol"; +import { ISuperMinterE, SuperMinterE } from "@modules/SuperMinterE.sol"; +import { DelegateCashLib } from "@modules/utils/DelegateCashLib.sol"; +import { LibOps } from "@core/utils/LibOps.sol"; +import { Ownable } from "solady/auth/Ownable.sol"; +import { WETH } from "solady/tokens/WETH.sol"; +import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import "../TestConfigV2_1.sol"; + +contract SuperMinterETests is TestConfigV2_1 { + SuperMinterE sm; + SoundEditionV2_1 edition; + Merkle merkle; + WETH weth; + + event Minted( + address indexed edition, + uint8 tier, + uint8 scheduleNum, + address indexed to, + ISuperMinterE.MintedLogData data, + uint256 indexed attributionId + ); + + struct SuperMinterEConstants { + uint128 MAX_PLATFORM_PER_TX_FLAT_FEE; + uint128 MAX_PER_MINT_REWARD; + uint16 MAX_PLATFORM_PER_MINT_FEE_BPS; + uint16 MAX_AFFILIATE_FEE_BPS; + } + + bytes constant DELEGATE_V2_REGISTRY_BYTECODE = + hex"60806040526004361061015e5760003560e01c80638988eea9116100c0578063b9f3687411610074578063d90e73ab11610059578063d90e73ab14610383578063e839bd5314610396578063e8e834a9146103b657600080fd5b8063b9f3687414610343578063ba63c8171461036357600080fd5b8063ac9650d8116100a5578063ac9650d8146102f0578063b18e2bbb14610310578063b87058751461032357600080fd5b80638988eea9146102bd578063ab764683146102dd57600080fd5b806335faa416116101175780634705ed38116100fc5780634705ed381461025d57806351525e9a1461027d57806361451a301461029d57600080fd5b806335faa4161461021957806342f87c251461023057600080fd5b806301ffc9a71161014857806301ffc9a7146101b6578063063182a5146101e657806330ff31401461020657600080fd5b80623c2ba61461016357806301a920a014610189575b600080fd5b6101766101713660046120b4565b6103d5565b6040519081526020015b60405180910390f35b34801561019557600080fd5b506101a96101a43660046120f6565b610637565b6040516101809190612118565b3480156101c257600080fd5b506101d66101d136600461215c565b61066e565b6040519015158152602001610180565b3480156101f257600080fd5b506101a96102013660046120f6565b6106e1565b6101766102143660046121ae565b610712565b34801561022557600080fd5b5061022e6108f9565b005b34801561023c57600080fd5b5061025061024b3660046120f6565b610917565b6040516101809190612219565b34801561026957600080fd5b50610250610278366004612368565b610948565b34801561028957600080fd5b506102506102983660046120f6565b610bf0565b3480156102a957600080fd5b506101a96102b8366004612368565b610c21565b3480156102c957600080fd5b506101d66102d83660046123aa565b610cc6565b6101766102eb3660046123f5565b610dd8565b6103036102fe366004612368565b611056565b6040516101809190612442565b61017661031e366004612510565b61118d565b34801561032f57600080fd5b5061017661033e366004612567565b6113bd565b34801561034f57600080fd5b506101d661035e366004612567565b6115d8565b34801561036f57600080fd5b5061017661037e3660046123aa565b611767565b6101766103913660046125bc565b61192d565b3480156103a257600080fd5b506101d66103b1366004612609565b611b3f565b3480156103c257600080fd5b506101766103d1366004612645565b5490565b60408051603c810185905260288101869052336014820152838152605c902060081b6004176000818152602081905291909120805473ffffffffffffffffffffffffffffffffffffffff1683156105865773ffffffffffffffffffffffffffffffffffffffff81166104ec57336000818152600160208181526040808420805480850182559085528285200188905573ffffffffffffffffffffffffffffffffffffffff8c1680855260028352818520805480860182559086529290942090910187905589901b7bffffffffffffffff000000000000000000000000000000000000000016909217845560a088901b17908301556104d582600486910155565b84156104e7576104e782600287910155565b6105d4565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff73ffffffffffffffffffffffffffffffffffffffff82160161055d5781547fffffffffffffffffffffffff000000000000000000000000000000000000000016331782556104e782600486910155565b3373ffffffffffffffffffffffffffffffffffffffff8216036104e7576104e782600486910155565b3373ffffffffffffffffffffffffffffffffffffffff8216036105d45781547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001178255600060048301555b604080518681526020810186905273ffffffffffffffffffffffffffffffffffffffff80891692908a169133917f6ebd000dfc4dc9df04f723f827bae7694230795e8f22ed4af438e074cc982d1891015b60405180910390a45050949350505050565b73ffffffffffffffffffffffffffffffffffffffff8116600090815260016020526040902060609061066890611bc2565b92915050565b60007f01ffc9a7000000000000000000000000000000000000000000000000000000007fffffffff0000000000000000000000000000000000000000000000000000000083169081147f5f68bc5a0000000000000000000000000000000000000000000000000000000090911417610668565b73ffffffffffffffffffffffffffffffffffffffff8116600090815260026020526040902060609061066890611bc2565b60408051602881018590523360148201528381526048902060081b6001176000818152602081905291909120805473ffffffffffffffffffffffffffffffffffffffff1683156108555773ffffffffffffffffffffffffffffffffffffffff81166107eb57336000818152600160208181526040808420805480850182559085528285200188905573ffffffffffffffffffffffffffffffffffffffff8b16808552600283529084208054808501825590855291909320018690559184559083015584156107e6576107e682600287910155565b61089c565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff73ffffffffffffffffffffffffffffffffffffffff8216016107e65781547fffffffffffffffffffffffff0000000000000000000000000000000000000000163317825561089c565b3373ffffffffffffffffffffffffffffffffffffffff82160361089c5781547fffffffffffffffffffffffff00000000000000000000000000000000000000001660011782555b60408051868152851515602082015273ffffffffffffffffffffffffffffffffffffffff88169133917fda3ef6410e30373a9137f83f9781a8129962b6882532b7c229de2e39de423227910160405180910390a350509392505050565b6000806000804770de1e80ea5a234fb5488fee2584251bc7e85af150565b73ffffffffffffffffffffffffffffffffffffffff8116600090815260026020526040902060609061066890611d41565b60608167ffffffffffffffff8111156109635761096361265e565b6040519080825280602002602001820160405280156109e857816020015b6040805160e08101825260008082526020808301829052928201819052606082018190526080820181905260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9092019101816109815790505b50905060005b82811015610be9576000610a25858584818110610a0d57610a0d61268d565b90506020020135600090815260208190526040902090565b90506000610a47825473ffffffffffffffffffffffffffffffffffffffff1690565b9050610a528161200f565b15610ab6576040805160e08101909152806000815260006020820181905260408201819052606082018190526080820181905260a0820181905260c0909101528451859085908110610aa657610aa661268d565b6020026020010181905250610bdf565b815460018301546040805160e08101825273ffffffffffffffffffffffffffffffffffffffff83169360a09390931c9290911c73ffffffffffffffff00000000000000000000000016919091179080610b278a8a89818110610b1a57610b1a61268d565b9050602002013560ff1690565b6005811115610b3857610b386121ea565b81526020018373ffffffffffffffffffffffffffffffffffffffff1681526020018473ffffffffffffffffffffffffffffffffffffffff168152602001610b80866002015490565b81526020018273ffffffffffffffffffffffffffffffffffffffff168152602001610bac866003015490565b8152602001610bbc866004015490565b815250868681518110610bd157610bd161268d565b602002602001018190525050505b50506001016109ee565b5092915050565b73ffffffffffffffffffffffffffffffffffffffff8116600090815260016020526040902060609061066890611d41565b6060818067ffffffffffffffff811115610c3d57610c3d61265e565b604051908082528060200260200182016040528015610c66578160200160208202803683370190505b50915060008060005b83811015610cbc57868682818110610c8957610c8961268d565b9050602002013592508254915081858281518110610ca957610ca961268d565b6020908102919091010152600101610c6f565b5050505092915050565b6000610cd18461200f565b610dcc576040805160288101879052601481018690526000808252604890912060081b6001178152602081905220610d0a905b85612035565b80610d4a575060408051603c810185905260288101879052601481018690526000808252605c90912060081b6002178152602081905220610d4a90610d04565b9050801515821517610dcc576040805160288101879052601481018690528381526048902060081b6001176000908152602081905220610d8990610d04565b80610dc9575060408051603c81018590526028810187905260148101869052838152605c902060081b6002176000908152602081905220610dc990610d04565b90505b80151560005260206000f35b60408051605c8101859052603c810186905260288101879052336014820152838152607c902060081b6005176000818152602081905291909120805473ffffffffffffffffffffffffffffffffffffffff168315610f9c5773ffffffffffffffffffffffffffffffffffffffff8116610f0257336000818152600160208181526040808420805480850182559085528285200188905573ffffffffffffffffffffffffffffffffffffffff8d168085526002835281852080548086018255908652929094209091018790558a901b7bffffffffffffffff000000000000000000000000000000000000000016909217845560a089901b1790830155610edf82600388910155565b610eeb82600486910155565b8415610efd57610efd82600287910155565b610fea565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff73ffffffffffffffffffffffffffffffffffffffff821601610f735781547fffffffffffffffffffffffff00000000000000000000000000000000000000001633178255610efd82600486910155565b3373ffffffffffffffffffffffffffffffffffffffff821603610efd57610efd82600486910155565b3373ffffffffffffffffffffffffffffffffffffffff821603610fea5781547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001178255600060048301555b604080518781526020810187905290810185905273ffffffffffffffffffffffffffffffffffffffff80891691908a169033907f27ab1adc9bca76301ed7a691320766dfa4b4b1aa32c9e05cf789611be7f8c75f906060015b60405180910390a4505095945050505050565b60608167ffffffffffffffff8111156110715761107161265e565b6040519080825280602002602001820160405280156110a457816020015b606081526020019060019003908161108f5790505b5090506000805b8381101561118557308585838181106110c6576110c661268d565b90506020028101906110d891906126bc565b6040516110e6929190612721565b600060405180830381855af49150503d8060008114611121576040519150601f19603f3d011682016040523d82523d6000602084013e611126565b606091505b508483815181106111395761113961268d565b602090810291909101015291508161117d576040517f4d6a232800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001016110ab565b505092915050565b60408051605c8101859052603c810186905260288101879052336014820152838152607c902060081b6003176000818152602081905291909120805473ffffffffffffffffffffffffffffffffffffffff1683156113155773ffffffffffffffffffffffffffffffffffffffff81166112ab57336000818152600160208181526040808420805480850182559085528285200188905573ffffffffffffffffffffffffffffffffffffffff8d168085526002835281852080548086018255908652929094209091018790558a901b7bffffffffffffffff000000000000000000000000000000000000000016909217845560a089901b179083015561129482600388910155565b84156112a6576112a682600287910155565b61135c565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff73ffffffffffffffffffffffffffffffffffffffff8216016112a65781547fffffffffffffffffffffffff0000000000000000000000000000000000000000163317825561135c565b3373ffffffffffffffffffffffffffffffffffffffff82160361135c5781547fffffffffffffffffffffffff00000000000000000000000000000000000000001660011782555b60408051878152602081018790528515159181019190915273ffffffffffffffffffffffffffffffffffffffff80891691908a169033907f15e7a1bdcd507dd632d797d38e60cc5a9c0749b9a63097a215c4d006126825c690606001611043565b60006113c88561200f565b6115ce576040805160288101889052601481018790526000808252604890912060081b6001178152602081905220611401905b86612035565b80611441575060408051603c810186905260288101889052601481018790526000808252605c90912060081b6002178152602081905220611441906113fb565b61148e5760408051605c8101859052603c810186905260288101889052601481018790526000808252607c90912060081b6005178152602081905220611489905b6004015490565b6114b0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81148215176115ce576040805160288101889052601481018790528381526048902060081b60011760009081526020819052908120611513905b87612035565b80611553575060408051603c81018790526028810189905260148101889052848152605c902060081b60021760009081526020819052206115539061150d565b61159d5760408051605c8101869052603c81018790526028810189905260148101889052848152607c902060081b600517600090815260208190522061159890611482565b6115bf565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b90508181108282180281189150505b8060005260206000f35b60006115e38561200f565b610dcc576040805160288101889052601481018790526000808252604890912060081b600117815260208190522061161a906113fb565b8061165a575060408051603c810186905260288101889052601481018790526000808252605c90912060081b600217815260208190522061165a906113fb565b806116a1575060408051605c8101859052603c810186905260288101889052601481018790526000808252607c90912060081b60031781526020819052206116a1906113fb565b9050801515821517610dcc576040805160288101889052601481018790528381526048902060081b60011760009081526020819052206116e0906113fb565b80611720575060408051603c81018690526028810188905260148101879052838152605c902060081b6002176000908152602081905220611720906113fb565b80610dc9575060408051605c8101859052603c81018690526028810188905260148101879052838152607c902060081b6003176000908152602081905220610dc9906113fb565b60006117728461200f565b6115ce576040805160288101879052601481018690526000808252604890912060081b60011781526020819052206117a990610d04565b806117e9575060408051603c810185905260288101879052601481018690526000808252605c90912060081b60021781526020819052206117e990610d04565b61182c5760408051603c810185905260288101879052601481018690526000808252605c90912060081b600417815260208190522061182790611482565b61184e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81148215176115ce576040805160288101879052601481018690528381526048902060081b600117600090815260208190529081206118af906113fb565b806118ef575060408051603c81018690526028810188905260148101879052848152605c902060081b60021760009081526020819052206118ef906113fb565b61159d5760408051603c81018690526028810188905260148101879052848152605c902060081b600417600090815260208190522061159890611482565b60408051603c810185905260288101869052336014820152838152605c902060081b6002176000818152602081905291909120805473ffffffffffffffffffffffffffffffffffffffff168315611aa25773ffffffffffffffffffffffffffffffffffffffff8116611a3857336000818152600160208181526040808420805480850182559085528285200188905573ffffffffffffffffffffffffffffffffffffffff8c1680855260028352818520805480860182559086529290942090910187905589901b7bffffffffffffffff000000000000000000000000000000000000000016909217845560a088901b17908301558415611a3357611a3382600287910155565b611ae9565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff73ffffffffffffffffffffffffffffffffffffffff821601611a335781547fffffffffffffffffffffffff00000000000000000000000000000000000000001633178255611ae9565b3373ffffffffffffffffffffffffffffffffffffffff821603611ae95781547fffffffffffffffffffffffff00000000000000000000000000000000000000001660011782555b60408051868152851515602082015273ffffffffffffffffffffffffffffffffffffffff80891692908a169133917f021be15e24de4afc43cfb5d0ba95ca38e0783571e05c12bbe6aece8842ae82df9101610625565b6000611b4a8361200f565b610dcc576040805160288101869052601481018590526000808252604890912060081b6001178152602081905220611b83905b84612035565b9050801515821517610dcc576040805160288101869052601481018590528381526048902060081b6001176000908152602081905220610dc990611b7d565b805460609060009081808267ffffffffffffffff811115611be557611be561265e565b604051908082528060200260200182016040528015611c0e578160200160208202803683370190505b50905060005b83811015611ca757868181548110611c2e57611c2e61268d565b90600052602060002001549250611c75611c70611c5685600090815260208190526040902090565b5473ffffffffffffffffffffffffffffffffffffffff1690565b61200f565b611c9f5782828680600101975081518110611c9257611c9261268d565b6020026020010181815250505b600101611c14565b508367ffffffffffffffff811115611cc157611cc161265e565b604051908082528060200260200182016040528015611cea578160200160208202803683370190505b50945060005b84811015611d3757818181518110611d0a57611d0a61268d565b6020026020010151868281518110611d2457611d2461268d565b6020908102919091010152600101611cf0565b5050505050919050565b805460609060009081808267ffffffffffffffff811115611d6457611d6461265e565b604051908082528060200260200182016040528015611d8d578160200160208202803683370190505b50905060005b83811015611e0757868181548110611dad57611dad61268d565b90600052602060002001549250611dd5611c70611c5685600090815260208190526040902090565b611dff5782828680600101975081518110611df257611df261268d565b6020026020010181815250505b600101611d93565b508367ffffffffffffffff811115611e2157611e2161265e565b604051908082528060200260200182016040528015611ea657816020015b6040805160e08101825260008082526020808301829052928201819052606082018190526080820181905260a0820181905260c082015282527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff909201910181611e3f5790505b5094506000805b8581101561200457828181518110611ec757611ec761268d565b60200260200101519350611ee684600090815260208190526040902090565b805460018201546040805160e08101825293955073ffffffffffffffffffffffffffffffffffffffff808416949083169360a09390931c9290911c73ffffffffffffffff0000000000000000000000001691909117908060ff89166005811115611f5257611f526121ea565b81526020018373ffffffffffffffffffffffffffffffffffffffff1681526020018473ffffffffffffffffffffffffffffffffffffffff168152602001611f9a876002015490565b81526020018273ffffffffffffffffffffffffffffffffffffffff168152602001611fc6876003015490565b8152602001611fd6876004015490565b8152508a8581518110611feb57611feb61268d565b6020026020010181905250505050806001019050611ead565b505050505050919050565b6000600173ffffffffffffffffffffffffffffffffffffffff8316908114901517610668565b6000612055835473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614905092915050565b803573ffffffffffffffffffffffffffffffffffffffff811681146120af57600080fd5b919050565b600080600080608085870312156120ca57600080fd5b6120d38561208b565b93506120e16020860161208b565b93969395505050506040820135916060013590565b60006020828403121561210857600080fd5b6121118261208b565b9392505050565b6020808252825182820181905260009190848201906040850190845b8181101561215057835183529284019291840191600101612134565b50909695505050505050565b60006020828403121561216e57600080fd5b81357fffffffff000000000000000000000000000000000000000000000000000000008116811461211157600080fd5b803580151581146120af57600080fd5b6000806000606084860312156121c357600080fd5b6121cc8461208b565b9250602084013591506121e16040850161219e565b90509250925092565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b60208082528251828201819052600091906040908185019086840185805b8381101561230e578251805160068110612278577f4e487b710000000000000000000000000000000000000000000000000000000084526021600452602484fd5b86528088015173ffffffffffffffffffffffffffffffffffffffff1688870152868101516122bd8888018273ffffffffffffffffffffffffffffffffffffffff169052565b506060818101519087015260808082015173ffffffffffffffffffffffffffffffffffffffff169087015260a0808201519087015260c0908101519086015260e09094019391860191600101612237565b509298975050505050505050565b60008083601f84011261232e57600080fd5b50813567ffffffffffffffff81111561234657600080fd5b6020830191508360208260051b850101111561236157600080fd5b9250929050565b6000806020838503121561237b57600080fd5b823567ffffffffffffffff81111561239257600080fd5b61239e8582860161231c565b90969095509350505050565b600080600080608085870312156123c057600080fd5b6123c98561208b565b93506123d76020860161208b565b92506123e56040860161208b565b9396929550929360600135925050565b600080600080600060a0868803121561240d57600080fd5b6124168661208b565b94506124246020870161208b565b94979496505050506040830135926060810135926080909101359150565b6000602080830181845280855180835260408601915060408160051b87010192508387016000805b83811015612502577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc089870301855282518051808852835b818110156124bd578281018a01518982018b015289016124a2565b508781018901849052601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690960187019550938601939186019160010161246a565b509398975050505050505050565b600080600080600060a0868803121561252857600080fd5b6125318661208b565b945061253f6020870161208b565b9350604086013592506060860135915061255b6080870161219e565b90509295509295909350565b600080600080600060a0868803121561257f57600080fd5b6125888661208b565b94506125966020870161208b565b93506125a46040870161208b565b94979396509394606081013594506080013592915050565b600080600080608085870312156125d257600080fd5b6125db8561208b565b93506125e96020860161208b565b9250604085013591506125fe6060860161219e565b905092959194509250565b60008060006060848603121561261e57600080fd5b6126278461208b565b92506126356020850161208b565b9150604084013590509250925092565b60006020828403121561265757600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18436030181126126f157600080fd5b83018035915067ffffffffffffffff82111561270c57600080fd5b60200191503681900382131561236157600080fd5b818382376000910190815291905056fea164736f6c6343000815000a"; + + function setUp() public virtual override { + super.setUp(); + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](2); + init.tierCreations[0].tier = 0; + init.tierCreations[1].tier = 1; + init.tierCreations[1].maxMintableLower = type(uint32).max; + init.tierCreations[1].maxMintableUpper = type(uint32).max; + edition = createSoundEdition(init); + sm = new SuperMinterE(); + edition.grantRoles(address(sm), edition.MINTER_ROLE()); + merkle = new Merkle(); + weth = new WETH(); + } + + function _superMinterConstants() internal view returns (SuperMinterEConstants memory smc) { + smc.MAX_PLATFORM_PER_TX_FLAT_FEE = sm.MAX_PLATFORM_PER_TX_FLAT_FEE(); + smc.MAX_PER_MINT_REWARD = sm.MAX_PER_MINT_REWARD(); + smc.MAX_PLATFORM_PER_MINT_FEE_BPS = sm.MAX_PLATFORM_PER_MINT_FEE_BPS(); + smc.MAX_AFFILIATE_FEE_BPS = sm.MAX_AFFILIATE_FEE_BPS(); + } + + function test_createMints() public { + uint256 gaPrice = 123 ether; + sm.setGAPrice(uint128(gaPrice)); + + assertEq(sm.mintInfoList(address(edition)).length, 0); + for (uint256 j; j < 3; ++j) { + for (uint256 i; i < 3; ++i) { + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.tier = uint8(i * 2); + c.price = uint128(i * 1 ether); + c.startTime = uint32(block.timestamp + i); + c.endTime = uint32(block.timestamp + 1000 + i); + c.maxMintablePerAccount = uint32(10 + i); + c.affiliateMerkleRoot = keccak256(abi.encodePacked(999 + j * 333 + i)); + if (i == 1) { + c.mode = sm.VERIFY_MERKLE(); + c.merkleRoot = keccak256("x"); + } + if (i == 2) { + c.mode = sm.VERIFY_SIGNATURE(); + } + uint8 nextScheduleNum = sm.nextScheduleNum(c.edition, c.tier); + assertEq(sm.createEditionMint(c), nextScheduleNum); + assertEq(nextScheduleNum, j); + assertEq(sm.mintInfoList(address(edition)).length, j * 3 + i + 1); + } + } + + address signer = _randomNonZeroAddress(); + sm.setPlatformSigner(signer); + + ISuperMinterE.MintInfo[] memory mintInfoList = sm.mintInfoList(address(edition)); + assertEq(mintInfoList.length, 3 * 3); + for (uint256 j; j < 3; ++j) { + for (uint256 i; i < 3; ++i) { + ISuperMinterE.MintInfo memory info = mintInfoList[j * 3 + i]; + assertEq(info.scheduleNum, j); + assertEq(info.edition, address(edition)); + assertEq(info.startTime, uint32(block.timestamp + i)); + assertEq(info.affiliateMerkleRoot, keccak256(abi.encodePacked(999 + j * 333 + i))); + if (i == 0) { + assertEq(info.mode, sm.DEFAULT()); + assertEq(info.price, gaPrice); + assertEq(info.maxMintablePerAccount, type(uint32).max); + assertEq(info.endTime, type(uint32).max); + assertEq(info.mode, sm.DEFAULT()); + assertEq(info.merkleRoot, bytes32(0)); + assertEq(info.signer, signer); + } + if (i == 1) { + assertEq(info.mode, sm.VERIFY_MERKLE()); + assertEq(info.price, i * 1 ether); + assertEq(info.maxMintablePerAccount, 10 + i); + assertEq(info.endTime, uint32(block.timestamp + 1000 + i)); + assertEq(info.mode, sm.VERIFY_MERKLE()); + assertEq(info.merkleRoot, keccak256("x")); + assertEq(info.signer, signer); + } + if (i == 2) { + assertEq(info.mode, sm.VERIFY_SIGNATURE()); + assertEq(info.price, i * 1 ether); + assertEq(info.maxMintablePerAccount, type(uint32).max); + assertEq(info.endTime, uint32(block.timestamp + 1000 + i)); + assertEq(info.mode, sm.VERIFY_SIGNATURE()); + assertEq(info.signer, signer); + } + } + } + } + + function test_settersConfigurable(uint256) public { + ISuperMinterE.MintCreation memory c; + c.maxMintable = uint32(_bound(_random(), 1, type(uint32).max)); + c.platform = address(this); + c.edition = address(edition); + c.tier = uint8(_random() % 2); + c.mode = uint8(_random() % 3); + c.price = uint128(_bound(_random(), 0, type(uint128).max)); + c.startTime = uint32(block.timestamp + _bound(_random(), 0, 1000)); + c.endTime = uint32(c.startTime + _bound(_random(), 0, 1000)); + c.maxMintablePerAccount = uint32(_bound(_random(), 1, type(uint32).max)); + c.merkleRoot = keccak256(abi.encodePacked(_random())); + + assertEq(sm.createEditionMint(c), 0); + + ISuperMinterE.MintInfo memory info = sm.mintInfo(address(edition), c.tier, 0); + assertEq(info.platform, address(this)); + if (c.tier == 0) { + if (c.mode == sm.DEFAULT()) { + assertEq(info.merkleRoot, bytes32(0)); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, 0); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, type(uint32).max); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, type(uint32).max); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } else if (c.mode == sm.VERIFY_MERKLE()) { + assertEq(info.merkleRoot, c.merkleRoot); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, 0); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, c.maxMintable); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, type(uint32).max); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } else if (c.mode == sm.VERIFY_SIGNATURE()) { + assertEq(info.signer, sm.platformSigner(c.platform)); + + assertEq(info.merkleRoot, bytes32(0)); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, c.price); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, c.maxMintable); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, type(uint32).max); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } + } else { + if (c.mode == sm.DEFAULT()) { + assertEq(info.merkleRoot, bytes32(0)); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, c.price); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, c.maxMintable); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, c.maxMintablePerAccount); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } else if (c.mode == sm.VERIFY_MERKLE()) { + assertEq(info.merkleRoot, c.merkleRoot); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, c.price); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, c.maxMintable); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, c.maxMintablePerAccount); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } else if (c.mode == sm.VERIFY_SIGNATURE()) { + assertEq(info.signer, sm.platformSigner(c.platform)); + + assertEq(info.merkleRoot, bytes32(0)); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, c.price); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, c.maxMintable); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, type(uint32).max); + vm.expectRevert(ISuperMinterE.NotConfigurable.selector); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } + } + } + + function test_platformAirdrop(uint256) public { + (address signer, uint256 privateKey) = _randomSigner(); + + ISuperMinterE.MintCreation memory c; + c.maxMintable = uint32(_bound(_random(), 1, 64)); + c.platform = address(this); + c.edition = address(edition); + c.startTime = 0; + c.tier = uint8(_random() % 2); + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = uint32(_random()); // Doesn't matter, will be auto set to max. + c.mode = sm.PLATFORM_AIRDROP(); + assertEq(sm.createEditionMint(c), 0); + + vm.prank(c.platform); + sm.setPlatformSigner(signer); + + unchecked { + ISuperMinterE.PlatformAirdrop memory p; + p.edition = address(edition); + p.tier = c.tier; + p.scheduleNum = 0; + p.to = new address[](_bound(_random(), 1, 8)); + p.signedQuantity = uint32(_bound(_random(), 1, 8)); + p.signedClaimTicket = uint32(_bound(_random(), 0, type(uint32).max)); + p.signedDeadline = type(uint32).max; + for (uint256 i; i < p.to.length; ++i) { + p.to[i] = _randomNonZeroAddress(); + } + LibSort.sort(p.to); + LibSort.uniquifySorted(p.to); + p.signature = _generatePlatformAirdropSignature(p, privateKey); + + uint256 expectedMinted = p.signedQuantity * p.to.length; + if (expectedMinted > c.maxMintable) { + vm.expectRevert(ISuperMinterE.ExceedsMintSupply.selector); + sm.platformAirdrop(p); + return; + } + + sm.platformAirdrop(p); + assertEq(sm.mintInfo(address(edition), p.tier, p.scheduleNum).minted, expectedMinted); + for (uint256 i; i < p.to.length; ++i) { + assertEq(edition.balanceOf(p.to[i]), p.signedQuantity); + assertEq(sm.numberMinted(address(edition), p.tier, p.scheduleNum, p.to[i]), p.signedQuantity); + } + + vm.expectRevert(ISuperMinterE.SignatureAlreadyUsed.selector); + sm.platformAirdrop(p); + } + } + + function test_mintDefaultUpToMaxPerAccount() public { + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.tier = 0; + c.price = 1 ether; + c.startTime = 0; + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = uint32(10); + assertEq(sm.createEditionMint(c), 0); + c.tier = 0; + assertEq(sm.createEditionMint(c), 1); + c.tier = 1; + assertEq(sm.createEditionMint(c), 0); + + ISuperMinterE.MintTo memory p; + p.edition = address(edition); + p.tier = 0; + p.scheduleNum = 0; + p.to = address(this); + p.quantity = 2; + + sm.mintTo{ value: 0 }(p); + assertEq(edition.balanceOf(address(this)), p.quantity); + assertEq(sm.numberMinted(address(edition), 0, 0, address(this)), p.quantity); + + p.tier = 1; + sm.mintTo{ value: p.quantity * 1 ether }(p); + assertEq(edition.balanceOf(address(this)), p.quantity * 2); + assertEq(sm.numberMinted(address(edition), 1, 0, address(this)), p.quantity); + + assertEq(edition.tokenTier(1), 0); + assertEq(edition.tokenTier(2), 0); + assertEq(edition.tokenTier(3), 1); + assertEq(edition.tokenTier(4), 1); + + assertEq(sm.mintInfo(address(edition), 0, 0).maxMintablePerAccount, type(uint32).max); + p.tier = 0; + p.quantity = 20; + sm.mintTo{ value: 0 }(p); + + p.tier = 1; + p.quantity = 9; + vm.expectRevert(ISuperMinterE.ExceedsMaxPerAccount.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + + p.quantity = 8; + sm.mintTo{ value: p.quantity * 1 ether }(p); + assertEq(edition.tierTokenIds(1).length, 10); + } + + function _twoRandomUniqueAddresses() internal returns (address[] memory c) { + c = new address[](2); + c[0] = _randomNonZeroAddress(); + do { + c[1] = _randomNonZeroAddress(); + } while (c[1] == c[0]); + } + + function test_mintMerkleUpToMaxPerAccount() public { + address[] memory allowlisted = _twoRandomUniqueAddresses(); + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = keccak256(abi.encodePacked(allowlisted[0])); + leaves[1] = keccak256(abi.encodePacked(allowlisted[1])); + + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.tier = 1; + c.mode = sm.VERIFY_MERKLE(); + c.merkleRoot = merkle.getRoot(leaves); + c.startTime = 0; + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = uint32(10); + c.price = 1 ether; + // Schedule 0. + assertEq(sm.createEditionMint(c), 0); + + ISuperMinterE.MintTo memory p; + p.edition = address(edition); + p.tier = 1; + p.scheduleNum = 0; + p.to = allowlisted[0]; + p.allowlisted = allowlisted[0]; + p.quantity = 2; + p.allowlistedQuantity = type(uint32).max; + p.allowlistProof = merkle.getProof(leaves, 0); + + // Try mint with a corrupted proof. + p.allowlistProof[0] = bytes32(uint256(p.allowlistProof[0]) ^ 1); + vm.expectRevert(ISuperMinterE.InvalidMerkleProof.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + // Restore the proof. + p.allowlistProof[0] = bytes32(uint256(p.allowlistProof[0]) ^ 1); + + sm.mintTo{ value: p.quantity * 1 ether }(p); + assertEq(edition.balanceOf(allowlisted[0]), p.quantity); + assertEq(edition.tokenTier(1), 1); + assertEq(edition.tokenTier(2), 1); + + assertEq(sm.numberMinted(address(edition), 1, 0, allowlisted[0]), p.quantity); + assertEq(sm.numberMinted(address(edition), 1, 0, allowlisted[1]), 0); + + p.quantity = 9; + vm.expectRevert(ISuperMinterE.ExceedsMaxPerAccount.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + + p.quantity = 8; + sm.mintTo{ value: p.quantity * 1 ether }(p); + + p.quantity = 1; + vm.expectRevert(ISuperMinterE.ExceedsMaxPerAccount.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + + // Schedule 1. + assertEq(sm.createEditionMint(c), 1); + + p.scheduleNum = 1; + p.quantity = 1; + sm.mintTo{ value: p.quantity * 1 ether }(p); + + leaves[0] = keccak256(abi.encodePacked(allowlisted[0], uint32(3))); + leaves[1] = keccak256(abi.encodePacked(allowlisted[1], uint32(3))); + + sm.setMerkleRoot(address(edition), 1, 1, merkle.getRoot(leaves)); + + p.allowlistProof = merkle.getProof(leaves, 0); + p.quantity = 3; + p.allowlistedQuantity = 3; + vm.expectRevert(ISuperMinterE.ExceedsMaxPerAccount.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + + p.quantity = 2; + sm.mintTo{ value: p.quantity * 1 ether }(p); + } + + function _setDelegateForAll(address delegate, bool value) internal { + if (address(DelegateCashLib.REGISTRY_V2).code.length == 0) { + vm.etch(DelegateCashLib.REGISTRY_V2, DELEGATE_V2_REGISTRY_BYTECODE); + } + (bool success, ) = address(DelegateCashLib.REGISTRY_V2).call( + abi.encodeWithSignature("delegateAll(address,bytes32,bool)", delegate, bytes32(0), value) + ); + assertTrue(success); + } + + function test_mintMerkleWithDelegate() public { + address[] memory allowlisted = _twoRandomUniqueAddresses(); + bytes32[] memory leaves = new bytes32[](2); + leaves[0] = keccak256(abi.encodePacked(allowlisted[0])); + leaves[1] = keccak256(abi.encodePacked(allowlisted[1])); + + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.tier = 1; + c.mode = sm.VERIFY_MERKLE(); + c.merkleRoot = merkle.getRoot(leaves); + c.startTime = 0; + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = uint32(10); + c.price = 1 ether; + // Schedule 0. + assertEq(sm.createEditionMint(c), 0); + + ISuperMinterE.MintTo memory p; + p.edition = address(edition); + p.tier = 1; + p.scheduleNum = 0; + p.to = address(this); + p.allowlisted = allowlisted[0]; + p.quantity = 1; + p.allowlistedQuantity = type(uint32).max; + p.allowlistProof = merkle.getProof(leaves, 0); + + uint256 expectedNFTBalance; + + vm.deal(allowlisted[0], 1000 ether); + + vm.expectRevert(ISuperMinterE.CallerNotDelegated.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + + vm.prank(allowlisted[0]); + sm.mintTo{ value: p.quantity * 1 ether }(p); + expectedNFTBalance += p.quantity; + + vm.expectRevert(ISuperMinterE.CallerNotDelegated.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + + for (uint256 q; q < 3; ++q) { + vm.prank(allowlisted[0]); + _setDelegateForAll(address(this), true); + + sm.mintTo{ value: p.quantity * 1 ether }(p); + expectedNFTBalance += p.quantity; + + vm.prank(allowlisted[0]); + _setDelegateForAll(address(this), false); + + vm.expectRevert(ISuperMinterE.CallerNotDelegated.selector); + sm.mintTo{ value: p.quantity * 1 ether }(p); + } + + assertEq(edition.balanceOf(address(this)), expectedNFTBalance); + } + + function test_platformFeeConfig() public { + uint8 tier = 12; + + _checkEffectivePlatformFeeConfig(tier, 0, 0, 0, false); + _checkDefaultPlatformFeeConfig(0, 0, 0, false); + + _setDefaultPlatformFeeConfig(1, 2, 3, true); + _checkDefaultPlatformFeeConfig(1, 2, 3, true); + _checkEffectivePlatformFeeConfig(tier, 1, 2, 3, true); + _setDefaultPlatformFeeConfig(1, 2, 3, false); + _checkDefaultPlatformFeeConfig(1, 2, 3, false); + _checkEffectivePlatformFeeConfig(tier, 0, 0, 0, false); + + _setPlatformFeeConfig(tier, 11, 22, 33, true); + _checkEffectivePlatformFeeConfig(tier, 11, 22, 33, true); + _setPlatformFeeConfig(tier, 11, 22, 33, false); + _checkEffectivePlatformFeeConfig(tier, 0, 0, 0, false); + _setDefaultPlatformFeeConfig(1, 2, 3, true); + _checkEffectivePlatformFeeConfig(tier, 1, 2, 3, true); + _setPlatformFeeConfig(tier, 11, 22, 33, true); + _checkEffectivePlatformFeeConfig(tier, 11, 22, 33, true); + } + + function test_platformFeeConfig(uint256) public { + SuperMinterEConstants memory smc = _superMinterConstants(); + uint128 perTxFlat = uint128(_bound(_random(), 0, smc.MAX_PLATFORM_PER_TX_FLAT_FEE * 2)); + uint128 platformReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD * 2)); + uint16 perMintBPS = uint16(_bound(_random(), 0, smc.MAX_PLATFORM_PER_MINT_FEE_BPS * 2)); + + uint8 tier = uint8(_random()); + bool active = _random() % 2 == 0; + + bool expectRevert = perTxFlat > smc.MAX_PLATFORM_PER_TX_FLAT_FEE || + platformReward > smc.MAX_PER_MINT_REWARD || + perMintBPS > smc.MAX_PLATFORM_PER_MINT_FEE_BPS; + + if (expectRevert) vm.expectRevert(ISuperMinterE.InvalidPlatformFeeConfig.selector); + + if (_random() % 2 == 0) { + _setPlatformFeeConfig(tier, perTxFlat, platformReward, perMintBPS, active); + if (!expectRevert) { + if (active) { + _checkEffectivePlatformFeeConfig(tier, perTxFlat, platformReward, perMintBPS, true); + } else { + _checkEffectivePlatformFeeConfig(tier, 0, 0, 0, false); + } + } + } else { + _setDefaultPlatformFeeConfig(perTxFlat, platformReward, perMintBPS, active); + if (!expectRevert) { + if (active) { + _checkEffectivePlatformFeeConfig(tier, perTxFlat, platformReward, perMintBPS, true); + _checkDefaultPlatformFeeConfig(perTxFlat, platformReward, perMintBPS, true); + } else { + _checkEffectivePlatformFeeConfig(tier, 0, 0, 0, false); + _checkDefaultPlatformFeeConfig(perTxFlat, platformReward, perMintBPS, false); + } + } + } + } + + function _setDefaultPlatformFeeConfig( + uint128 perTxFlat, + uint128 platformReward, + uint16 perMintBPS, + bool active + ) internal { + ISuperMinterE.PlatformFeeConfig memory c; + c.platformTxFlatFee = perTxFlat; + c.platformMintReward = platformReward; + c.platformMintFeeBPS = perMintBPS; + c.active = active; + sm.setDefaultPlatformFeeConfig(c); + } + + function _setPlatformFeeConfig( + uint8 tier, + uint128 perTxFlat, + uint128 platformReward, + uint16 perMintBPS, + bool active + ) internal { + ISuperMinterE.PlatformFeeConfig memory c; + c.platformTxFlatFee = perTxFlat; + c.platformMintReward = platformReward; + c.platformMintFeeBPS = perMintBPS; + c.active = active; + sm.setPlatformFeeConfig(tier, c); + } + + function _checkDefaultPlatformFeeConfig( + uint128 perTxFlat, + uint128 platformReward, + uint16 perMintBPS, + bool active + ) internal { + _checkPlatformFeeConfig( + sm.defaultPlatformFeeConfig(address(this)), + perTxFlat, + platformReward, + perMintBPS, + active + ); + } + + function _checkEffectivePlatformFeeConfig( + uint8 tier, + uint128 perTxFlat, + uint128 platformReward, + uint16 perMintBPS, + bool active + ) internal { + _checkPlatformFeeConfig( + sm.effectivePlatformFeeConfig(address(this), tier), + perTxFlat, + platformReward, + perMintBPS, + active + ); + } + + function _checkPlatformFeeConfig( + ISuperMinterE.PlatformFeeConfig memory result, + uint128 perTxFlat, + uint128 platformReward, + uint16 perMintBPS, + bool active + ) internal { + assertEq(result.platformTxFlatFee, perTxFlat); + assertEq(result.platformMintReward, platformReward); + assertEq(result.platformMintFeeBPS, perMintBPS); + assertEq(result.active, active); + } + + function test_unitPrice(uint256) public { + SuperMinterEConstants memory smc = _superMinterConstants(); + + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = _randomNonZeroAddress(); + c.edition = address(edition); + c.tier = uint8(_random() % 2); + c.mode = uint8(_random() % 3); + c.price = uint128(_bound(_random(), 0, type(uint128).max)); + c.affiliateFeeBPS = uint16(_bound(_random(), 0, smc.MAX_AFFILIATE_FEE_BPS)); + c.startTime = 0; + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = type(uint32).max; + if (c.mode == sm.VERIFY_MERKLE()) { + c.merkleRoot = bytes32(_random() | 1); + } + assertEq(sm.createEditionMint(c), 0); + + uint256 gaPrice = uint128(_bound(_random(), 0, type(uint128).max)); + vm.prank(c.platform); + sm.setGAPrice(uint128(gaPrice)); + + uint32 quantity = uint32(_bound(_random(), 1, type(uint32).max)); + uint128 signedPrice = uint128(_bound(_random(), 1, type(uint128).max)); + ISuperMinterE.TotalPriceAndFees memory tpaf; + if (c.mode == sm.VERIFY_SIGNATURE() && signedPrice < c.price) { + vm.expectRevert(ISuperMinterE.SignedPriceTooLow.selector); + tpaf = sm.totalPriceAndFeesWithSignedPrice(address(edition), c.tier, 0, quantity, signedPrice, false); + signedPrice = c.price; + } + tpaf = sm.totalPriceAndFeesWithSignedPrice(address(edition), c.tier, 0, quantity, signedPrice, false); + if (c.mode == sm.VERIFY_SIGNATURE()) { + assertEq(tpaf.unitPrice, signedPrice); + } else if (c.tier == 0) { + assertEq(tpaf.unitPrice, gaPrice); + } else { + assertEq(tpaf.unitPrice, c.price); + } + + ISuperMinterE.MintInfo memory info = sm.mintInfo(address(edition), c.tier, 0); + if (c.tier == 0) { + assertEq(info.price, c.mode == sm.VERIFY_SIGNATURE() ? c.price : gaPrice); + } else { + assertEq(info.price, c.price); + } + } + + function test_mintWithVariousFees(uint256) public { + SuperMinterEConstants memory smc = _superMinterConstants(); + address[] memory feeRecipients = _twoRandomUniqueAddresses(); + + // Create a tier 1 mint schedule, without any affiliate root. + ISuperMinterE.MintCreation memory c; + { + c.maxMintable = type(uint32).max; + c.platform = _randomNonZeroAddress(); + c.edition = address(edition); + c.tier = 1; + c.price = uint128(_bound(_random(), 0, type(uint128).max)); + c.affiliateFeeBPS = uint16(_bound(_random(), 0, smc.MAX_AFFILIATE_FEE_BPS)); + c.startTime = 0; + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = type(uint32).max; + assertEq(sm.createEditionMint(c), 0); + } + + // Set the tier 1 platform fee config. + ISuperMinterE.PlatformFeeConfig memory pfc; + { + pfc.platformTxFlatFee = uint128(_bound(_random(), 0, smc.MAX_PLATFORM_PER_TX_FLAT_FEE)); + pfc.platformMintFeeBPS = uint16(_bound(_random(), 0, smc.MAX_PLATFORM_PER_MINT_FEE_BPS)); + + pfc.artistMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.affiliateMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.platformMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + + pfc.thresholdPrice = uint128(_bound(_random(), 0, type(uint128).max)); + + pfc.thresholdArtistMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.thresholdAffiliateMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.thresholdPlatformMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + + pfc.active = true; + vm.prank(c.platform); + sm.setPlatformFeeConfig(1, pfc); + } + + // Prepare the MintTo struct witha a random quantity. + ISuperMinterE.MintTo memory p; + { + p.edition = address(edition); + p.tier = 1; + p.scheduleNum = 0; + p.to = address(this); + p.quantity = uint32(_bound(_random(), 0, type(uint32).max)); + } + + // Just to ensure we have enough ETH to mint. + vm.deal(address(this), type(uint192).max); + + ISuperMinterE.MintedLogData memory l; + ISuperMinterE.TotalPriceAndFees memory tpaf; + { + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, _random() % 2 == 0); + assertEq(tpaf.subTotal, c.price * uint256(p.quantity)); + assertGt(tpaf.total + 1, tpaf.subTotal); + + // Use a lower, non-zero quantity for mint testing. + p.quantity = uint32(_bound(_random(), 1, 8)); + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, _random() % 2 == 0); + assertEq(tpaf.subTotal, c.price * uint256(p.quantity)); + assertGt(tpaf.total + 1, tpaf.subTotal); + } + + // Test the affiliated path. + if (_random() % 2 == 0) { + p.affiliate = _randomNonZeroAddress(); + + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, true); + + vm.expectEmit(true, true, true, true); + + l.quantity = p.quantity; + l.fromTokenId = 1; + l.affiliate = p.affiliate; + l.affiliated = true; + l.requiredPayment = tpaf.total; + l.unitPrice = tpaf.unitPrice; + l.finalArtistFee = tpaf.finalArtistFee; + l.finalAffiliateFee = tpaf.finalAffiliateFee; + l.finalPlatformFee = tpaf.finalPlatformFee; + + emit Minted(address(edition), c.tier, 0, address(this), l, 0); + } else { + p.affiliate = address(0); + + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, false); + + vm.expectEmit(true, true, true, true); + + l.quantity = p.quantity; + l.fromTokenId = 1; + l.affiliate = address(0); + l.affiliated = false; + l.requiredPayment = tpaf.total; + l.unitPrice = tpaf.unitPrice; + l.finalArtistFee = tpaf.finalArtistFee; + l.finalAffiliateFee = 0; + l.finalPlatformFee = tpaf.finalPlatformFee; + + emit Minted(address(edition), c.tier, 0, address(this), l, 0); + } + + sm.mintTo{ value: tpaf.total }(p); + + // Check invariants. + assertEq(l.finalPlatformFee + l.finalAffiliateFee + l.finalArtistFee, tpaf.total); + assertEq(sm.platformFeesAccrued(c.platform), l.finalPlatformFee); + assertEq(sm.affiliateFeesAccrued(p.affiliate), l.finalAffiliateFee); + assertEq(address(sm).balance, l.finalPlatformFee + l.finalAffiliateFee); + assertEq(address(edition).balance, l.finalArtistFee); + + // Perform the withdrawals for affiliate and check if the balances tally. + uint256 balanceBefore = address(p.affiliate).balance; + sm.withdrawForAffiliate(p.affiliate); + assertEq(address(p.affiliate).balance, balanceBefore + l.finalAffiliateFee); + + // Perform the withdrawals for platform and check if the balances tally. + balanceBefore = address(feeRecipients[0]).balance; + vm.prank(c.platform); + sm.setPlatformFeeAddress(feeRecipients[0]); + assertEq(sm.platformFeeAddress(c.platform), feeRecipients[0]); + sm.withdrawForPlatform(c.platform); + assertEq(address(feeRecipients[0]).balance, balanceBefore + l.finalPlatformFee); + assertEq(sm.platformFeeAddress(c.platform), feeRecipients[0]); + assertEq(address(sm).balance, 0); + } + + function test_mintWithVariousERC20Fees(uint256) public { + SuperMinterEConstants memory smc = _superMinterConstants(); + address[] memory feeRecipients = _twoRandomUniqueAddresses(); + + // Create a tier 1 mint schedule, without any affiliate root. + ISuperMinterE.MintCreation memory c; + { + c.maxMintable = type(uint32).max; + c.platform = _randomNonZeroAddress(); + c.edition = address(edition); + c.tier = 1; + c.price = uint128(_bound(_random(), 0, type(uint128).max)); + c.affiliateFeeBPS = uint16(_bound(_random(), 0, smc.MAX_AFFILIATE_FEE_BPS)); + c.startTime = 0; + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = type(uint32).max; + c.erc20 = address(weth); + assertEq(sm.createEditionMint(c), 0); + } + + // Set the tier 1 platform fee config. + ISuperMinterE.PlatformFeeConfig memory pfc; + { + pfc.platformTxFlatFee = uint128(_bound(_random(), 0, smc.MAX_PLATFORM_PER_TX_FLAT_FEE)); + pfc.platformMintFeeBPS = uint16(_bound(_random(), 0, smc.MAX_PLATFORM_PER_MINT_FEE_BPS)); + + pfc.artistMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.affiliateMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.platformMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + + pfc.thresholdPrice = uint128(_bound(_random(), 0, type(uint128).max)); + + pfc.thresholdArtistMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.thresholdAffiliateMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.thresholdPlatformMintReward = uint128(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + + pfc.active = true; + vm.prank(c.platform); + sm.setPlatformFeeConfig(1, pfc); + } + + // Prepare the MintTo struct witha a random quantity. + ISuperMinterE.MintTo memory p; + { + p.edition = address(edition); + p.tier = 1; + p.scheduleNum = 0; + p.to = address(this); + p.quantity = uint32(_bound(_random(), 0, type(uint32).max)); + } + + // Just to ensure we have enough ETH to mint. + vm.deal(address(this), type(uint192).max); + weth.deposit{ value: type(uint192).max }(); + assertEq(weth.balanceOf(address(this)), type(uint192).max); + weth.approve(address(sm), type(uint256).max); + + ISuperMinterE.MintedLogData memory l; + ISuperMinterE.TotalPriceAndFees memory tpaf; + { + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, _random() % 2 == 0); + assertEq(tpaf.subTotal, c.price * uint256(p.quantity)); + assertGt(tpaf.total + 1, tpaf.subTotal); + + // Use a lower, non-zero quantity for mint testing. + p.quantity = uint32(_bound(_random(), 1, 8)); + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, _random() % 2 == 0); + assertEq(tpaf.subTotal, c.price * uint256(p.quantity)); + assertGt(tpaf.total + 1, tpaf.subTotal); + } + + // Test the affiliated path. + if (_random() % 2 == 0) { + p.affiliate = _randomNonZeroAddress(); + + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, true); + + vm.expectEmit(true, true, true, true); + + l.quantity = p.quantity; + l.fromTokenId = 1; + l.affiliate = p.affiliate; + l.affiliated = true; + l.requiredPayment = tpaf.total; + l.unitPrice = tpaf.unitPrice; + l.finalArtistFee = tpaf.finalArtistFee; + l.finalAffiliateFee = tpaf.finalAffiliateFee; + l.finalPlatformFee = tpaf.finalPlatformFee; + l.erc20 = address(weth); + + emit Minted(address(edition), c.tier, 0, address(this), l, 0); + } else { + p.affiliate = address(0); + + tpaf = sm.totalPriceAndFees(address(edition), c.tier, 0, p.quantity, false); + + vm.expectEmit(true, true, true, true); + + l.quantity = p.quantity; + l.fromTokenId = 1; + l.affiliate = address(0); + l.affiliated = false; + l.requiredPayment = tpaf.total; + l.unitPrice = tpaf.unitPrice; + l.finalArtistFee = tpaf.finalArtistFee; + l.finalAffiliateFee = 0; + l.finalPlatformFee = tpaf.finalPlatformFee; + l.erc20 = address(weth); + + emit Minted(address(edition), c.tier, 0, address(this), l, 0); + } + + sm.mintTo(p); + + // Check invariants. + assertEq(l.finalPlatformFee + l.finalAffiliateFee + l.finalArtistFee, tpaf.total); + assertEq(sm.platformERC20FeesAccrued(c.platform, address(weth)), l.finalPlatformFee); + assertEq(sm.affiliateERC20FeesAccrued(p.affiliate, address(weth)), l.finalAffiliateFee); + assertEq(weth.balanceOf(address(sm)), l.finalPlatformFee + l.finalAffiliateFee); + assertEq(weth.balanceOf(address(edition)), l.finalArtistFee); + + // Perform the withdrawals for affiliate and check if the balances tally. + uint256 balanceBefore = address(p.affiliate).balance; + sm.withdrawERC20ForAffiliate(p.affiliate, address(weth)); + assertEq(weth.balanceOf(address(p.affiliate)), balanceBefore + l.finalAffiliateFee); + + // Perform the withdrawals for platform and check if the balances tally. + balanceBefore = weth.balanceOf(address(feeRecipients[0])); + vm.prank(c.platform); + sm.setPlatformFeeAddress(feeRecipients[0]); + assertEq(sm.platformFeeAddress(c.platform), feeRecipients[0]); + sm.withdrawERC20ForPlatform(c.platform, address(weth)); + assertEq(weth.balanceOf(address(feeRecipients[0])), balanceBefore + l.finalPlatformFee); + assertEq(sm.platformFeeAddress(c.platform), feeRecipients[0]); + assertEq(weth.balanceOf(address(sm)), 0); + } + + function test_mintWithSignature(uint256) public { + (address signer, uint256 privateKey) = _randomSigner(); + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = _randomNonZeroAddress(); + c.edition = address(edition); + c.tier = uint8(_random() % 2); + c.startTime = 0; + c.mode = sm.VERIFY_SIGNATURE(); + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = type(uint32).max; + vm.prank(c.platform); + sm.setPlatformSigner(signer); + assertEq(sm.createEditionMint(c), 0); + + ISuperMinterE.MintTo memory p; + p.edition = address(edition); + p.tier = c.tier; + p.scheduleNum = 0; + p.to = _randomNonZeroAddress(); + p.quantity = uint32(_bound(_random(), 1, 16)); + p.signedPrice = uint128(_bound(_random(), 0, type(uint128).max)); + p.signedQuantity = uint32(p.quantity + (_random() % 16)); + p.signedClaimTicket = uint32(_bound(_random(), 0, type(uint32).max)); + p.signedDeadline = type(uint32).max; + p.affiliate = _randomNonZeroAddress(); + while (p.affiliate == p.to) p.affiliate = _randomNonZeroAddress(); + p.signature = _generateSignature(p, privateKey); + + vm.deal(address(this), type(uint192).max); + + sm.mintTo{ value: uint256(p.quantity) * uint256(p.signedPrice) }(p); + + vm.expectRevert(ISuperMinterE.SignatureAlreadyUsed.selector); + sm.mintTo{ value: uint256(p.quantity) * uint256(p.signedPrice) }(p); + + assertEq(edition.balanceOf(p.to), p.quantity); + + p.signedClaimTicket = uint32(p.signedClaimTicket ^ 1); + vm.expectRevert(ISuperMinterE.InvalidSignature.selector); + sm.mintTo{ value: uint256(p.quantity) * uint256(p.signedPrice) }(p); + + uint32 originalQuantity = p.quantity; + p.quantity = uint32(p.signedQuantity + _bound(_random(), 1, 10)); + p.signature = _generateSignature(p, privateKey); + vm.expectRevert(ISuperMinterE.ExceedsSignedQuantity.selector); + sm.mintTo{ value: uint256(p.quantity) * uint256(p.signedPrice) }(p); + p.quantity = originalQuantity; + + p.signature = _generateSignature(p, privateKey); + sm.mintTo{ value: uint256(p.quantity) * uint256(p.signedPrice) }(p); + + assertEq(edition.balanceOf(p.to), p.quantity * 2); + } + + function _generateSignature(ISuperMinterE.MintTo memory p, uint256 privateKey) + internal + returns (bytes memory signature) + { + bytes32 digest = sm.computeMintToDigest(p); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + signature = abi.encodePacked(r, s, v); + } + + function _generatePlatformAirdropSignature(ISuperMinterE.PlatformAirdrop memory p, uint256 privateKey) + internal + returns (bytes memory signature) + { + bytes32 digest = sm.computePlatformAirdropDigest(p); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + signature = abi.encodePacked(r, s, v); + } + + function test_mintGA(uint256) public { + ISuperMinterE.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = _randomNonZeroAddress(); + c.edition = address(edition); + c.tier = 0; + c.startTime = 0; + c.mode = sm.DEFAULT(); + c.endTime = uint32(block.timestamp + 1000); + c.maxMintablePerAccount = type(uint32).max; + assertEq(sm.createEditionMint(c), 0); + + uint256 gaPrice = uint128(_bound(_random(), 0, type(uint128).max)); + vm.prank(c.platform); + sm.setGAPrice(uint128(gaPrice)); + + ISuperMinterE.MintTo memory p; + p.edition = address(edition); + p.tier = 0; + p.scheduleNum = 0; + p.to = _randomNonZeroAddress(); + p.quantity = uint32(_bound(_random(), 1, 16)); + + vm.deal(address(this), type(uint192).max); + sm.mintTo{ value: uint256(p.quantity) * uint256(gaPrice) }(p); + } +}