diff --git a/.changeset/modern-shirts-try.md b/.changeset/modern-shirts-try.md new file mode 100644 index 00000000..e94209e4 --- /dev/null +++ b/.changeset/modern-shirts-try.md @@ -0,0 +1,5 @@ +--- +"@soundxyz/sound-protocol": minor +--- + +SuperMinterV2 et al. diff --git a/.gitignore b/.gitignore index 23d1efd4..65b0743b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ cache out* typechain dist +create2 +.tmp # Ignores development broadcast logs /broadcast/**/run-166*.json @@ -16,4 +18,4 @@ node_modules lcov.info /coverage/* -.DS_Store \ No newline at end of file +.DS_Store diff --git a/.gitmodules b/.gitmodules index d5129750..57d90663 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,11 +26,9 @@ url = https://github.com/chiru-labs/ERC721A-Upgradeable branch = 05bd2b9993e632ff898472fb6aec6d698a4c6015 ignore = dirty -[submodule "lib/multicaller"] - path = lib/multicaller - url = https://github.com/vectorized/multicaller - branch = main - ignore = dirty [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady +[submodule "lib/multicaller"] + path = lib/multicaller + url = https://github.com/vectorized/multicaller diff --git a/build_create2_deployments.sh b/build_create2_deployments.sh index 5412b742..f8151fef 100755 --- a/build_create2_deployments.sh +++ b/build_create2_deployments.sh @@ -61,3 +61,7 @@ generateDeployment "SoundEditionV2"; generateDeployment "SoundCreatorV2"; generateDeployment "SoundOnChainMetadata"; generateDeployment "SoundMetadata"; + +generateDeployment "SoundEditionV2_1"; +generateDeployment "SuperMinterV2"; + diff --git a/contracts/core/SoundEditionV2_1.sol b/contracts/core/SoundEditionV2_1.sol new file mode 100644 index 00000000..6751dcae --- /dev/null +++ b/contracts/core/SoundEditionV2_1.sol @@ -0,0 +1,1082 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; +import { ERC721AUpgradeable, ERC721AStorage } from "chiru-labs/ERC721A-Upgradeable/ERC721AUpgradeable.sol"; +import { ERC721AQueryableUpgradeable } from "chiru-labs/ERC721A-Upgradeable/extensions/ERC721AQueryableUpgradeable.sol"; +import { ERC721ABurnableUpgradeable } from "chiru-labs/ERC721A-Upgradeable/extensions/ERC721ABurnableUpgradeable.sol"; +import { IERC2981Upgradeable } from "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { LibString } from "solady/utils/LibString.sol"; +import { LibMap } from "solady/utils/LibMap.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; +import { ISoundEditionV2_1 } from "./interfaces/ISoundEditionV2_1.sol"; +import { IMetadataModule } from "./interfaces/IMetadataModule.sol"; + +import { LibOps } from "./utils/LibOps.sol"; +import { ArweaveURILib } from "./utils/ArweaveURILib.sol"; +import { MintRandomnessLib } from "./utils/MintRandomnessLib.sol"; + +/** + * @title SoundEditionV2_1 + * @notice The Sound Edition contract - a creator-owned, modifiable implementation of ERC721A. + */ +contract SoundEditionV2_1 is ISoundEditionV2_1, ERC721AQueryableUpgradeable, ERC721ABurnableUpgradeable, OwnableRoles { + using ArweaveURILib for ArweaveURILib.URI; + using LibMap for *; + + // ============================================================= + // STRUCTS + // ============================================================= + + /** + * @dev A struct containing the tier data in storage. + */ + struct TierData { + // The current mint randomness state. + uint64 mintRandomness; + // The lower bound of the maximum number of tokens that can be minted for the tier. + uint32 maxMintableLower; + // The upper bound of the maximum number of tokens that can be minted for the tier. + uint32 maxMintableUpper; + // The timestamp (in seconds since unix epoch) after which the + // max amount of tokens mintable for the tier will drop from + // `maxMintableUpper` to `maxMintableLower`. + uint32 cutoffTime; + // The total number of tokens minted for the tier. + uint32 minted; + // The offset to the next tier data in the linked list. + uint8 next; + // Packed boolean flags. + uint8 flags; + } + + // ============================================================= + // CONSTANTS + // ============================================================= + + /** + * @dev The GA tier. Which is 0. + */ + uint8 public constant GA_TIER = 0; + + /** + * @dev A role every minter module must have in order to mint new tokens. + * Note: this constant will always be 2 for past and future sound protocol contracts. + */ + uint256 public constant MINTER_ROLE = LibOps.MINTER_ROLE; + + /** + * @dev A role the owner can grant for performing admin actions. + * Note: this constant will always be 1 for past and future sound protocol contracts. + */ + uint256 public constant ADMIN_ROLE = LibOps.ADMIN_ROLE; + + /** + * @dev Basis points denominator used in fee calculations. + */ + uint16 public constant BPS_DENOMINATOR = LibOps.BPS_DENOMINATOR; + + /** + * @dev The interface ID for EIP-2981 (royaltyInfo) + */ + bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a; + + /** + * @dev The boolean flag on whether the metadata is frozen. + */ + uint8 private constant _METADATA_IS_FROZEN_FLAG = 1 << 0; + + /** + * @dev The boolean flag on whether the ability to create a new tier is frozen. + */ + uint8 private constant _CREATE_TIER_IS_FROZEN_FLAG = 1 << 1; + + /** + * @dev The boolean flag on whether the tier has been created. + */ + uint8 private constant _TIER_CREATED_FLAG = 1 << 0; + + /** + * @dev The boolean flag on whether the tier has mint randomness enabled. + */ + uint8 private constant _TIER_MINT_RANDOMNESS_ENABLED_FLAG = 1 << 1; + + /** + * @dev The boolean flag on whether the tier is frozen. + */ + uint8 private constant _TIER_IS_FROZEN_FLAG = 1 << 2; + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev The value for `name` and `symbol` if their combined + * length is (32 - 2) bytes. We need 2 bytes for their lengths. + */ + bytes32 private _shortNameAndSymbol; + + /** + * @dev The metadata's base URI. + */ + ArweaveURILib.URI private _baseURIStorage; + + /** + * @dev The contract base URI. + */ + ArweaveURILib.URI private _contractURIStorage; + + /** + * @dev The destination for ETH withdrawals. + */ + address public fundingRecipient; + + /** + * @dev The royalty fee in basis points. + */ + uint16 public royaltyBPS; + + /** + * @dev Packed boolean flags. + */ + uint8 private _flags; + + /** + * @dev Metadata module used for `tokenURI` and `contractURI` if it is set. + */ + address public metadataModule; + + /** + * @dev The total number of tiers. + */ + uint16 private _numTiers; + + /** + * @dev The head of the tier data linked list. + */ + uint8 private _tierDataHead; + + /** + * @dev A mapping of `tier` => `tierData`. + */ + mapping(uint256 => TierData) private _tierData; + + /** + * @dev A packed mapping `tokenId` => `tier`. + */ + LibMap.Uint8Map private _tokenTiers; + + /** + * @dev A packed mapping of `tier` => `index` => `tokenId`. + */ + mapping(uint256 => LibMap.Uint32Map) private _tierTokenIds; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function initialize(EditionInitialization memory init) public { + // Will revert upon double initialization. + _initializeERC721A(init.name, init.symbol); + _initializeOwner(LibMulticaller.senderOrSigner()); + + _validateRoyaltyBPS(init.royaltyBPS); + _validateFundingRecipient(init.fundingRecipient); + + _baseURIStorage.initialize(init.baseURI); + _contractURIStorage.initialize(init.contractURI); + + fundingRecipient = init.fundingRecipient; + + unchecked { + uint256 n = init.tierCreations.length; + if (n == 0) revert ZeroTiersProvided(); + for (uint256 i; i != n; ++i) { + _createTier(init.tierCreations[i]); + } + } + + metadataModule = init.metadataModule; + royaltyBPS = init.royaltyBPS; + + _flags = + LibOps.toFlag(init.isMetadataFrozen, _METADATA_IS_FROZEN_FLAG) | + LibOps.toFlag(init.isCreateTierFrozen, _CREATE_TIER_IS_FROZEN_FLAG); + + emit SoundEditionInitialized(init); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function mint( + uint8 tier, + address to, + uint256 quantity + ) external payable onlyRolesOrOwner(ADMIN_ROLE | MINTER_ROLE) returns (uint256 fromTokenId) { + uint32 fromTierTokenIdIndex; + (fromTokenId, fromTierTokenIdIndex) = _beforeTieredMint(tier, quantity); + _batchMint(to, quantity); + emit Minted(tier, to, quantity, fromTokenId, fromTierTokenIdIndex); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function airdrop( + uint8 tier, + address[] calldata to, + uint256 quantity + ) external payable onlyRolesOrOwner(ADMIN_ROLE | MINTER_ROLE) returns (uint256 fromTokenId) { + uint32 fromTierTokenIdIndex; + unchecked { + // Multiplication overflow is not possible due to the max block gas limit. + // If `quantity` is too big (e.g. 2**64), the loop in `_batchMint` will run out of gas. + // If `to.length` is too big (e.g. 2**64), the airdrop mint loop will run out of gas. + (fromTokenId, fromTierTokenIdIndex) = _beforeTieredMint(tier, to.length * quantity); + for (uint256 i; i != to.length; ++i) { + _batchMint(to[i], quantity); + } + } + emit Airdropped(tier, to, quantity, fromTokenId, fromTierTokenIdIndex); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function withdrawETH() external { + uint256 amount = address(this).balance; + address recipient = fundingRecipient; + SafeTransferLib.forceSafeTransferETH(recipient, amount); + emit ETHWithdrawn(recipient, amount, msg.sender); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function withdrawERC20(address[] calldata tokens) external { + unchecked { + uint256[] memory amounts = new uint256[](tokens.length); + address recipient = fundingRecipient; + for (uint256 i; i != tokens.length; ++i) { + amounts[i] = SafeTransferLib.safeTransferAll(tokens[i], recipient); + } + emit ERC20Withdrawn(recipient, tokens, amounts, msg.sender); + } + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setMetadataModule(address module) external onlyRolesOrOwner(ADMIN_ROLE) { + _requireMetadataNotFrozen(); + metadataModule = module; + emit MetadataModuleSet(module); + emitAllMetadataUpdate(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setBaseURI(string memory uri) external onlyRolesOrOwner(ADMIN_ROLE) { + _requireMetadataNotFrozen(); + _baseURIStorage.update(uri); + emit BaseURISet(uri); + emitAllMetadataUpdate(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setContractURI(string memory uri) public onlyRolesOrOwner(ADMIN_ROLE) { + _requireMetadataNotFrozen(); + _contractURIStorage.update(uri); + emit ContractURISet(uri); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function freezeMetadata() public onlyRolesOrOwner(ADMIN_ROLE) { + _requireMetadataNotFrozen(); + _flags |= _METADATA_IS_FROZEN_FLAG; + emit MetadataFrozen(metadataModule, baseURI(), contractURI()); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function freezeCreateTier() public onlyRolesOrOwner(ADMIN_ROLE) { + _requireCreateTierNotFrozen(); + _flags |= _CREATE_TIER_IS_FROZEN_FLAG; + emit CreateTierFrozen(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setFundingRecipient(address recipient) public onlyRolesOrOwner(ADMIN_ROLE) { + _setFundingRecipient(recipient); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function createSplit(address splitMain, bytes calldata splitData) + public + onlyRolesOrOwner(ADMIN_ROLE) + returns (address split) + { + assembly { + // Grab the free memory pointer. + let m := mload(0x40) + // Copy the `splitData` into the free memory. + calldatacopy(m, splitData.offset, splitData.length) + // Zeroize 0x00, so that if the call doesn't return anything, `split` will be the zero address. + mstore(0x00, 0) + // Call the `splitMain`, reverting if the call fails. + if iszero( + call( + gas(), // Gas remaining. + splitMain, // Address of the SplitMain. + 0, // Send 0 ETH. + m, // Start of the `splitData` in memory. + splitData.length, // Length of `splitData`. + 0x00, // Start of returndata. + 0x20 // Length of returndata. + ) + ) { + // Bubble up the revert if the call reverts. + returndatacopy(0x00, 0x00, returndatasize()) + revert(0x00, returndatasize()) + } + split := mload(0x00) + } + _setFundingRecipient(split); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setRoyalty(uint16 bps) public onlyRolesOrOwner(ADMIN_ROLE) { + _validateRoyaltyBPS(bps); + royaltyBPS = bps; + emit RoyaltySet(bps); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setMaxMintableRange( + uint8 tier, + uint32 lower, + uint32 upper + ) public onlyRolesOrOwner(ADMIN_ROLE) { + TierData storage d = _getTierData(tier); + _requireNotFrozen(d); + _requireBeforeMintConcluded(d); + uint256 minted = d.minted; + + if (minted != 0) { + // Disallow increasing either lower or upper. + if (LibOps.or(lower > d.maxMintableLower, upper > d.maxMintableUpper)) revert InvalidMaxMintableRange(); + // If either is below `minted`, set to `minted`. + lower = uint32(LibOps.max(lower, minted)); + upper = uint32(LibOps.max(upper, minted)); + } + + if (lower > upper) revert InvalidMaxMintableRange(); + + d.maxMintableLower = lower; + d.maxMintableUpper = upper; + + emit MaxMintableRangeSet(tier, lower, upper); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function freezeTier(uint8 tier) public onlyRolesOrOwner(ADMIN_ROLE) { + TierData storage d = _getTierData(tier); + _requireNotFrozen(d); + d.flags |= _TIER_IS_FROZEN_FLAG; + emit TierFrozen(tier); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setCutoffTime(uint8 tier, uint32 cutoff) public onlyRolesOrOwner(ADMIN_ROLE) { + TierData storage d = _getTierData(tier); + _requireNotFrozen(d); + _requireBeforeMintConcluded(d); + d.cutoffTime = cutoff; + emit CutoffTimeSet(tier, cutoff); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function createTier(TierCreation memory creation) public onlyRolesOrOwner(ADMIN_ROLE) { + _requireCreateTierNotFrozen(); + _createTier(creation); + emit TierCreated(creation); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function setMintRandomnessEnabled(uint8 tier, bool enabled) public onlyRolesOrOwner(ADMIN_ROLE) { + TierData storage d = _getTierData(tier); + _requireNotFrozen(d); + _requireNoTierMints(d); + d.flags = LibOps.setFlagTo(d.flags, _TIER_MINT_RANDOMNESS_ENABLED_FLAG, enabled); + emit MintRandomnessEnabledSet(tier, enabled); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function emitAllMetadataUpdate() public { + emit BatchMetadataUpdate(_startTokenId(), _nextTokenId() - 1); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function editionInfo() public view returns (EditionInfo memory info) { + info.baseURI = baseURI(); + info.contractURI = contractURI(); + (info.name, info.symbol) = _loadNameAndSymbol(); + info.fundingRecipient = fundingRecipient; + info.metadataModule = metadataModule; + info.isMetadataFrozen = isMetadataFrozen(); + info.isCreateTierFrozen = isCreateTierFrozen(); + info.royaltyBPS = royaltyBPS; + info.nextTokenId = nextTokenId(); + info.totalMinted = totalMinted(); + info.totalBurned = totalBurned(); + info.totalSupply = totalSupply(); + + unchecked { + uint256 n = _numTiers; // Linked-list length. + uint8 p = _tierDataHead; // Current linked-list pointer. + info.tierInfo = new TierInfo[](n); + // Traverse the linked-list and fill the array in reverse. + // Front: earliest added tier. Back: latest added tier. + while (n != 0) { + TierData storage d = _getTierData(p); + info.tierInfo[--n] = tierInfo(p); + p = d.next; + } + } + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tierInfo(uint8 tier) public view returns (TierInfo memory info) { + TierData storage d = _getTierData(tier); + info.tier = tier; + info.maxMintable = _maxMintable(d); + info.maxMintableLower = d.maxMintableLower; + info.maxMintableUpper = d.maxMintableUpper; + info.cutoffTime = d.cutoffTime; + info.minted = d.minted; + info.mintRandomness = _mintRandomness(d); + info.mintRandomnessEnabled = _mintRandomnessEnabled(d); + info.mintConcluded = _mintConcluded(d); + info.isFrozen = _isFrozen(d); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function isFrozen(uint8 tier) public view returns (bool) { + return _isFrozen(_getTierData(tier)); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function isMetadataFrozen() public view returns (bool) { + return _flags & _METADATA_IS_FROZEN_FLAG != 0; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function isCreateTierFrozen() public view returns (bool) { + return _flags & _CREATE_TIER_IS_FROZEN_FLAG != 0; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function nextTokenId() public view returns (uint256) { + return _nextTokenId(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function numberMinted(address owner) public view returns (uint256) { + return _numberMinted(owner); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function numberBurned(address owner) public view returns (uint256) { + return _numberBurned(owner); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function totalMinted() public view returns (uint256) { + return _totalMinted(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function totalBurned() public view returns (uint256) { + return _totalBurned(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tokenTier(uint256 tokenId) public view returns (uint8) { + if (!_exists(tokenId)) revert TierQueryForNonexistentToken(); + return _tokenTiers.get(tokenId); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function explicitTokenTier(uint256 tokenId) public view returns (uint8) { + return _tokenTiers.get(tokenId); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tokenTiers(uint256[] calldata tokenIds) public view returns (uint8[] memory tiers) { + unchecked { + tiers = new uint8[](tokenIds.length); + for (uint256 i; i != tokenIds.length; ++i) { + tiers[i] = _tokenTiers.get(tokenIds[i]); + } + } + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tierMinted(uint8 tier) public view returns (uint32) { + return _getTierData(tier).minted; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tierTokenIds(uint8 tier) public view returns (uint256[] memory tokenIds) { + tokenIds = tierTokenIdsIn(tier, 0, tierMinted(tier)); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tierTokenIdsIn( + uint8 tier, + uint256 start, + uint256 stop + ) public view returns (uint256[] memory tokenIds) { + unchecked { + uint256 l = stop - start; + uint256 n = tierMinted(tier); + if (LibOps.or(start > stop, stop > n)) revert InvalidQueryRange(); + tokenIds = new uint256[](l); + LibMap.Uint32Map storage m = _tierTokenIds[tier]; + for (uint256 i; i != l; ++i) { + tokenIds[i] = m.get(start + i); + } + } + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function tierTokenIdIndex(uint256 tokenId) public view returns (uint256) { + uint8 tier = tokenTier(tokenId); + (bool found, uint256 index) = _tierTokenIds[tier].searchSorted(uint32(tokenId), 0, tierMinted(tier)); + return LibOps.and(tokenId < 1 << 32, found) ? index : type(uint256).max; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function mintRandomness(uint8 tier) public view returns (uint256 result) { + return _mintRandomness(_getTierData(tier)); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function mintConcluded(uint8 tier) public view returns (bool) { + return _mintConcluded(_getTierData(tier)); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function maxMintable(uint8 tier) public view returns (uint32) { + return _maxMintable(_getTierData(tier)); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function maxMintableUpper(uint8 tier) public view returns (uint32) { + return _getTierData(tier).maxMintableUpper; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function maxMintableLower(uint8 tier) public view returns (uint32) { + return _getTierData(tier).maxMintableLower; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function cutoffTime(uint8 tier) public view returns (uint32) { + return _getTierData(tier).cutoffTime; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function mintRandomnessEnabled(uint8 tier) public view returns (bool) { + return _mintRandomnessEnabled(_getTierData(tier)); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function mintRandomnessOneOfOne(uint8 tier) public view returns (uint32) { + TierData storage d = _getTierData(tier); + uint256 r = _mintRandomness(d); + uint256 n = _maxMintable(d); + return LibOps.or(r == 0, n == 0) ? 0 : _tierTokenIds[tier].get(LibOps.rawMod(r, n)); + } + + /** + * @inheritdoc IERC721AUpgradeable + */ + function tokenURI(uint256 tokenId) + public + view + override(ERC721AUpgradeable, IERC721AUpgradeable) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + return explicitTokenURI(tokenId); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function explicitTokenURI(uint256 tokenId) public view returns (string memory) { + if (metadataModule != address(0)) return IMetadataModule(metadataModule).tokenURI(tokenId); + string memory baseURI_ = baseURI(); + return bytes(baseURI_).length != 0 ? string.concat(baseURI_, _toString(tokenId)) : ""; + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function supportsInterface(bytes4 interfaceId) + public + view + override(ISoundEditionV2_1, ERC721AUpgradeable, IERC721AUpgradeable) + returns (bool) + { + return + LibOps.or( + interfaceId == type(ISoundEditionV2_1).interfaceId, + ERC721AUpgradeable.supportsInterface(interfaceId), + interfaceId == _INTERFACE_ID_ERC2981 + ); + } + + /** + * @inheritdoc IERC2981Upgradeable + */ + function royaltyInfo( + uint256, // tokenId + uint256 salePrice + ) public view override(IERC2981Upgradeable) returns (address recipient, uint256 royaltyAmount) { + recipient = fundingRecipient; + if (salePrice >= 1 << 240) LibOps.revertOverflow(); // `royaltyBPS` is uint16. `256 - 16 = 240`. + royaltyAmount = LibOps.rawMulDiv(salePrice, royaltyBPS, BPS_DENOMINATOR); + } + + /** + * @inheritdoc IERC721AUpgradeable + */ + function name() public view override(ERC721AUpgradeable, IERC721AUpgradeable) returns (string memory name_) { + (name_, ) = _loadNameAndSymbol(); + } + + /** + * @inheritdoc IERC721AUpgradeable + */ + function symbol() public view override(ERC721AUpgradeable, IERC721AUpgradeable) returns (string memory symbol_) { + (, symbol_) = _loadNameAndSymbol(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function baseURI() public view returns (string memory) { + return _baseURIStorage.load(); + } + + /** + * @inheritdoc ISoundEditionV2_1 + */ + function contractURI() public view returns (string memory) { + return _contractURIStorage.load(); + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + /** + * @dev Override the `onlyRolesOrOwner` modifier on `OwnableRoles` + * to support multicaller sender forwarding. + */ + modifier onlyRolesOrOwner(uint256 roles) virtual override { + _requireOnlyRolesOrOwner(roles); + _; + } + + /** + * @dev Require that the caller has any of the `roles`, or is the owner of the contract. + * @param roles A roles bitmap. + */ + function _requireOnlyRolesOrOwner(uint256 roles) internal view { + address sender = LibMulticaller.senderOrSigner(); + if (!hasAnyRole(sender, roles)) + if (sender != owner()) LibOps.revertUnauthorized(); + } + + /** + * @inheritdoc ERC721AUpgradeable + */ + function _startTokenId() internal pure override returns (uint256) { + return 1; + } + + /** + * @dev Ensures the royalty basis points is a valid value. + * @param bps The royalty BPS. + */ + function _validateRoyaltyBPS(uint16 bps) internal pure { + if (bps > BPS_DENOMINATOR) revert InvalidRoyaltyBPS(); + } + + /** + * @dev Ensures the funding recipient is not the zero address. + * @param recipient The funding recipient. + */ + function _validateFundingRecipient(address recipient) internal pure { + if (recipient == address(0)) revert InvalidFundingRecipient(); + } + + /** + * @dev Reverts if the metadata is frozen. + */ + function _requireMetadataNotFrozen() internal view { + if (isMetadataFrozen()) revert MetadataIsFrozen(); + } + + /** + * @dev Reverts if the max tier is frozen. + */ + function _requireCreateTierNotFrozen() internal view { + if (isCreateTierFrozen()) revert CreateTierIsFrozen(); + } + + /** + * @dev Reverts if there are any mints. + */ + function _requireNoMints() internal view { + if (_totalMinted() != 0) revert MintsAlreadyExist(); + } + + /** + * @dev Reverts if there are any mints for the tier. + * @param d The tier data. + */ + function _requireNoTierMints(TierData storage d) internal view { + if (d.minted != 0) revert TierMintsAlreadyExist(); + } + + /** + * @dev Create a new tier. + * @param c The tier creation struct. + */ + function _createTier(TierCreation memory c) internal { + uint8 tier = c.tier; + TierData storage d = _tierData[tier]; + if (d.flags & _TIER_CREATED_FLAG != 0) revert TierAlreadyExists(); + + // If GA, overwrite any immutable variables as required. + if (tier == GA_TIER) { + c.maxMintableLower = type(uint32).max; + c.maxMintableUpper = type(uint32).max; + c.cutoffTime = type(uint32).max; + c.mintRandomnessEnabled = false; + c.isFrozen = true; + } else { + if (c.maxMintableLower > c.maxMintableUpper) revert InvalidMaxMintableRange(); + } + + d.maxMintableLower = c.maxMintableLower; + d.maxMintableUpper = c.maxMintableUpper; + d.cutoffTime = c.cutoffTime; + d.flags = + _TIER_CREATED_FLAG | + LibOps.toFlag(c.mintRandomnessEnabled, _TIER_MINT_RANDOMNESS_ENABLED_FLAG) | + LibOps.toFlag(c.isFrozen, _TIER_IS_FROZEN_FLAG); + + unchecked { + uint16 n = uint16(uint256(_numTiers) + 1); // `_numTiers` is uint16. `tier` is uint8. + d.next = _tierDataHead; + _numTiers = n; + _tierDataHead = tier; + } + } + + /** + * @dev Sets the funding recipient address. + * @param recipient Address to be set as the new funding recipient. + */ + function _setFundingRecipient(address recipient) internal { + _validateFundingRecipient(recipient); + fundingRecipient = recipient; + emit FundingRecipientSet(recipient); + } + + /** + * @dev Ensures that the tier is not frozen. + * @param d The tier data. + */ + function _requireNotFrozen(TierData storage d) internal view { + if (_isFrozen(d)) revert TierIsFrozen(); + } + + /** + * @dev Ensures that the mint has not been concluded. + * @param d The tier data. + */ + function _requireBeforeMintConcluded(TierData storage d) internal view { + if (_mintConcluded(d)) revert MintHasConcluded(); + } + + /** + * @dev Ensures that the mint has been concluded. + * @param d The tier data. + */ + function _requireAfterMintConcluded(TierData storage d) internal view { + if (!_mintConcluded(d)) revert MintNotConcluded(); + } + + /** + * @dev Append to the tier token IDs and the token tiers arrays. + * Reverts if there is insufficient supply. + * @param tier The tier. + * @param quantity The total number of tokens to mint. + * @return fromTokenId The first token ID minted. + * @return fromTierTokenIdIndex The first token index in the tier. + */ + function _beforeTieredMint(uint8 tier, uint256 quantity) + internal + returns (uint256 fromTokenId, uint32 fromTierTokenIdIndex) + { + unchecked { + if (quantity == 0) revert MintZeroQuantity(); + fromTokenId = _nextTokenId(); + + // To ensure that we won't store a token ID above 2**31 - 1 in `_tierTokenIds`. + if (fromTokenId + quantity - 1 >= 1 << 32) LibOps.revertOverflow(); + + TierData storage d = _getTierData(tier); + + uint256 minted = d.minted; // uint32. + uint256 limit = _maxMintable(d); // uint32. + + // Check that the mints will not exceed the available supply. + uint256 finalMinted = minted + quantity; + if (finalMinted > limit) revert ExceedsAvailableSupply(); + + d.minted = uint32(finalMinted); + + // Update the mint randomness state if required. + if (_mintRandomnessEnabled(d)) + d.mintRandomness = uint64( + MintRandomnessLib.nextMintRandomness(d.mintRandomness, minted, quantity, limit) + ); + + LibMap.Uint32Map storage m = _tierTokenIds[tier]; + for (uint256 i; i != quantity; ++i) { + m.set(minted + i, uint32(fromTokenId + i)); // Set the token IDs for the tier. + if (tier != 0) _tokenTiers.set(fromTokenId + i, tier); // Set the tier for the token ID. + } + fromTierTokenIdIndex = uint32(minted); + } + } + + /** + * @dev Returns the full mint randomness for the tier. + * @param d The tier data. + * @return result The full mint randomness. + */ + function _mintRandomness(TierData storage d) internal view returns (uint256 result) { + if (_mintRandomnessEnabled(d) && _mintConcluded(d)) { + result = d.mintRandomness; + assembly { + mstore(0x00, result) + mstore(0x20, address()) + result := keccak256(0x00, 0x40) + result := add(iszero(result), result) + } + } + } + + /** + * @dev Returns whether the mint has concluded for the tier. + * @param d The tier data. + * @return Whether the mint has concluded. + */ + function _mintConcluded(TierData storage d) internal view returns (bool) { + return d.minted >= _maxMintable(d); + } + + /** + * @dev Returns whether the mint has mint randomness enabled. + * @param d The tier data. + * @return Whether mint randomness is enabled. + */ + function _mintRandomnessEnabled(TierData storage d) internal view returns (bool) { + return d.flags & _TIER_MINT_RANDOMNESS_ENABLED_FLAG != 0; + } + + /** + * @dev Returns the current max mintable supply for the tier. + * @param d The tier data. + * @return The current max mintable supply. + */ + function _maxMintable(TierData storage d) internal view returns (uint32) { + if (block.timestamp < d.cutoffTime) return d.maxMintableUpper; + return uint32(LibOps.max(d.maxMintableLower, d.minted)); + } + + /** + * @dev Returns whether the tier is frozen. + * @param d The tier data. + * @return Whether the tier is frozen. + */ + function _isFrozen(TierData storage d) internal view returns (bool) { + return d.flags & _TIER_IS_FROZEN_FLAG != 0; + } + + /** + * @dev Returns a storage pointer to the tier data, reverting if the tier does not exist. + * @param tier The tier. + * @return d A storage pointer to the tier data. + */ + function _getTierData(uint8 tier) internal view returns (TierData storage d) { + d = _tierData[tier]; + if (d.flags & _TIER_CREATED_FLAG == 0) revert TierDoesNotExist(); + } + + /** + * @dev Helper function for initializing the ERC721A class. + * @param name_ Name of the collection. + * @param symbol_ Symbol of the collection. + */ + function _initializeERC721A(string memory name_, string memory symbol_) internal { + ERC721AStorage.Layout storage layout = ERC721AStorage.layout(); + + // Prevent double initialization. + // We can "cheat" here and avoid the initializer modifier to save a SSTORE, + // since the `_nextTokenId()` is defined to always return 1. + if (layout._currentIndex != 0) LibOps.revertUnauthorized(); + layout._currentIndex = _startTokenId(); + + // Returns `bytes32(0)` if the strings are too long to be packed into a single word. + bytes32 packed = LibString.packTwo(name_, symbol_); + // If we cannot pack both strings into a single 32-byte word, store separately. + // We need 2 bytes to store their lengths. + if (packed == bytes32(0)) { + layout._name = name_; + layout._symbol = symbol_; + } else { + // Otherwise, pack them and store them into a single word. + _shortNameAndSymbol = packed; + } + } + + /** + * @dev Helper function for retrieving the name and symbol, + * unpacking them from a single word in storage if previously packed. + * @return name_ Name of the collection. + * @return symbol_ Symbol of the collection. + */ + function _loadNameAndSymbol() internal view returns (string memory name_, string memory symbol_) { + bytes32 packed = _shortNameAndSymbol; + // If the strings have been previously packed. + if (packed != bytes32(0)) { + (name_, symbol_) = LibString.unpackTwo(packed); + } else { + // Otherwise, load them from their separate variables. + ERC721AStorage.Layout storage layout = ERC721AStorage.layout(); + name_ = layout._name; + symbol_ = layout._symbol; + } + } + + /** + * @dev Mints a big batch in mini batches to prevent expensive + * first-time transfer gas costs. + * @param to The address to mint to. + * @param quantity The number of NFTs to mint. + */ + function _batchMint(address to, uint256 quantity) internal { + unchecked { + // Mint in mini batches of 32. + uint256 i = quantity % 32; + if (i != 0) _mint(to, i); + while (i != quantity) { + _mint(to, 32); + i += 32; + } + } + } +} diff --git a/contracts/core/interfaces/ISoundEditionV2_1.sol b/contracts/core/interfaces/ISoundEditionV2_1.sol new file mode 100644 index 00000000..85b7ee12 --- /dev/null +++ b/contracts/core/interfaces/ISoundEditionV2_1.sol @@ -0,0 +1,813 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; +import { IERC2981Upgradeable } from "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import { IERC165Upgradeable } from "openzeppelin-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +import { IMetadataModule } from "./IMetadataModule.sol"; + +/** + * @title ISoundEditionV2_1 + * @notice The interface for Sound edition contracts. + */ +interface ISoundEditionV2_1 is IERC721AUpgradeable, IERC2981Upgradeable { + // ============================================================= + // STRUCTS + // ============================================================= + + /** + * @dev The information pertaining to a tier. + */ + struct TierInfo { + // The tier. + uint8 tier; + // The current max mintable amount. + uint32 maxMintable; + // The lower bound of the maximum number of tokens that can be minted for the tier. + uint32 maxMintableLower; + // The upper bound of the maximum number of tokens that can be minted for the tier. + uint32 maxMintableUpper; + // The timestamp (in seconds since unix epoch) after which the + // max amount of tokens mintable for the tier will drop from + // `maxMintableUpper` to `maxMintableLower`. + uint32 cutoffTime; + // The total number of tokens minted for the tier. + uint32 minted; + // The mint randomness for the tier. + uint256 mintRandomness; + // Whether the tier mints have concluded. + bool mintConcluded; + // Whether the tier has mint randomness enabled. + bool mintRandomnessEnabled; + // Whether the tier is frozen. + bool isFrozen; + } + + /** + * @dev A struct containing the arguments for creating a tier. + */ + struct TierCreation { + // The tier. + uint8 tier; + // The lower bound of the maximum number of tokens that can be minted for the tier. + uint32 maxMintableLower; + // The upper bound of the maximum number of tokens that can be minted for the tier. + uint32 maxMintableUpper; + // The timestamp (in seconds since unix epoch) after which the + // max amount of tokens mintable for the tier will drop from + // `maxMintableUpper` to `maxMintableLower`. + uint32 cutoffTime; + // Whether the tier has mint randomness enabled. + bool mintRandomnessEnabled; + // Whether the tier is frozen. + bool isFrozen; + } + + /** + * @dev The information pertaining to this edition. + */ + struct EditionInfo { + // Base URI for the metadata. + string baseURI; + // Contract URI for OpenSea storefront. + string contractURI; + // Name of the collection. + string name; + // Symbol of the collection. + string symbol; + // Address that receives primary and secondary royalties. + address fundingRecipient; + // Address of the metadata module. Optional. + address metadataModule; + // Whether the metadata is frozen. + bool isMetadataFrozen; + // Whether the ability to create tiers is frozen. + bool isCreateTierFrozen; + // The royalty BPS (basis points). + uint16 royaltyBPS; + // Next token ID to be minted. + uint256 nextTokenId; + // Total number of tokens burned. + uint256 totalBurned; + // Total number of tokens minted. + uint256 totalMinted; + // Total number of tokens currently in existence. + uint256 totalSupply; + // An array of tier info. From lowest (0-indexed) to highest. + TierInfo[] tierInfo; + } + + /** + * @dev A struct containing the arguments for initialization. + */ + struct EditionInitialization { + // Name of the collection. + string name; + // Symbol of the collection. + string symbol; + // Address of the metadata module. Optional. + address metadataModule; + // Base URI for the metadata. + string baseURI; + // Contract URI for OpenSea storefront. + string contractURI; + // Address that receives primary and secondary royalties. + address fundingRecipient; + // The royalty BPS (basis points). + uint16 royaltyBPS; + // Whether the metadata is frozen. + bool isMetadataFrozen; + // Whether the ability to create tiers is frozen. + bool isCreateTierFrozen; + // An array of tier creation structs. From lowest (0-indexed) to highest. + TierCreation[] tierCreations; + } + + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when the metadata module is set. + * @param metadataModule the address of the metadata module. + */ + event MetadataModuleSet(address metadataModule); + + /** + * @dev Emitted when the `baseURI` is set. + * @param baseURI the base URI of the edition. + */ + event BaseURISet(string baseURI); + + /** + * @dev Emitted when the `contractURI` is set. + * @param contractURI The contract URI of the edition. + */ + event ContractURISet(string contractURI); + + /** + * @dev Emitted when the metadata is frozen (e.g.: `baseURI` can no longer be changed). + * @param metadataModule The address of the metadata module. + * @param baseURI The base URI of the edition. + * @param contractURI The contract URI of the edition. + */ + event MetadataFrozen(address metadataModule, string baseURI, string contractURI); + + /** + * @dev Emitted when the ability to create tier is removed. + */ + event CreateTierFrozen(); + + /** + * @dev Emitted when the `fundingRecipient` is set. + * @param recipient The address of the funding recipient. + */ + event FundingRecipientSet(address recipient); + + /** + * @dev Emitted when the `royaltyBPS` is set. + * @param bps The new royalty, measured in basis points. + */ + event RoyaltySet(uint16 bps); + + /** + * @dev Emitted when the tier's maximum mintable token quantity range is set. + * @param tier The tier. + * @param lower The lower limit of the maximum number of tokens that can be minted. + * @param upper The upper limit of the maximum number of tokens that can be minted. + */ + event MaxMintableRangeSet(uint8 tier, uint32 lower, uint32 upper); + + /** + * @dev Emitted when the tier's cutoff time set. + * @param tier The tier. + * @param cutoff The timestamp. + */ + event CutoffTimeSet(uint8 tier, uint32 cutoff); + + /** + * @dev Emitted when the `mintRandomnessEnabled` for the tier is set. + * @param tier The tier. + * @param enabled The boolean value. + */ + event MintRandomnessEnabledSet(uint8 tier, bool enabled); + + /** + * @dev Emitted upon initialization. + * @param init The initialization data. + */ + event SoundEditionInitialized(EditionInitialization init); + + /** + * @dev Emitted when a tier is created. + * @param creation The tier creation data. + */ + event TierCreated(TierCreation creation); + + /** + * @dev Emitted when a tier is frozen. + * @param tier The tier. + */ + event TierFrozen(uint8 tier); + + /** + * @dev Emitted upon ETH withdrawal. + * @param recipient The recipient of the withdrawal. + * @param amount The amount withdrawn. + * @param caller The account that initiated the withdrawal. + */ + event ETHWithdrawn(address recipient, uint256 amount, address caller); + + /** + * @dev Emitted upon ERC20 withdrawal. + * @param recipient The recipient of the withdrawal. + * @param tokens The addresses of the ERC20 tokens. + * @param amounts The amount of each token withdrawn. + * @param caller The account that initiated the withdrawal. + */ + event ERC20Withdrawn(address recipient, address[] tokens, uint256[] amounts, address caller); + + /** + * @dev Emitted upon a mint. + * @param tier The tier. + * @param to The address to mint to. + * @param quantity The number of minted. + * @param fromTokenId The first token ID minted. + * @param fromTierTokenIdIndex The first token index in the tier. + */ + event Minted(uint8 tier, address to, uint256 quantity, uint256 fromTokenId, uint32 fromTierTokenIdIndex); + + /** + * @dev Emitted upon an airdrop. + * @param tier The tier. + * @param to The recipients of the airdrop. + * @param quantity The number of tokens airdropped to each address in `to`. + * @param fromTokenId The first token ID minted to the first address in `to`. + * @param fromTierTokenIdIndex The first token index in the tier. + */ + event Airdropped(uint8 tier, address[] to, uint256 quantity, uint256 fromTokenId, uint32 fromTierTokenIdIndex); + + /** + * @dev EIP-4906 event to signal marketplaces to refresh the metadata. + * @param fromTokenId The starting token ID. + * @param toTokenId The ending token ID. + */ + event BatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The edition's metadata is frozen (e.g.: `baseURI` can no longer be changed). + */ + error MetadataIsFrozen(); + + /** + * @dev The ability to create tiers is frozen. + */ + error CreateTierIsFrozen(); + + /** + * @dev The given `royaltyBPS` is invalid. + */ + error InvalidRoyaltyBPS(); + + /** + * @dev A minimum of one tier must be provided to initialize a Sound Edition. + */ + error ZeroTiersProvided(); + + /** + * @dev The requested quantity exceeds the edition's remaining mintable token quantity. + */ + error ExceedsAvailableSupply(); + + /** + * @dev The given `fundingRecipient` address is invalid. + */ + error InvalidFundingRecipient(); + + /** + * @dev The `maxMintableLower` must not be greater than `maxMintableUpper`. + */ + error InvalidMaxMintableRange(); + + /** + * @dev The mint has already concluded. + */ + error MintHasConcluded(); + + /** + * @dev The mint has not concluded. + */ + error MintNotConcluded(); + + /** + * @dev Cannot perform the operation after a token has been minted. + */ + error MintsAlreadyExist(); + + /** + * @dev Cannot perform the operation after a token has been minted in the tier. + */ + error TierMintsAlreadyExist(); + + /** + * @dev The token IDs must be in strictly ascending order. + */ + error TokenIdsNotStrictlyAscending(); + + /** + * @dev The tier does not exist. + */ + error TierDoesNotExist(); + + /** + * @dev The tier already exists. + */ + error TierAlreadyExists(); + + /** + * @dev The tier is frozen. + */ + error TierIsFrozen(); + + /** + * @dev One of more of the tokens do not have the correct token tier. + */ + error InvalidTokenTier(); + + /** + * @dev Please wait for a while before you burn. + */ + error CannotBurnImmediately(); + + /** + * @dev The token for the tier query doesn't exist. + */ + error TierQueryForNonexistentToken(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Initializes the contract. + * @param init The initialization struct. + */ + function initialize(EditionInitialization calldata init) external; + + /** + * @dev Mints `quantity` tokens to addrress `to` + * Each token will be assigned a token ID that is consecutively increasing. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have either the + * `ADMIN_ROLE`, `MINTER_ROLE`, which can be granted via {grantRole}. + * Multiple minters, such as different minter contracts, + * can be authorized simultaneously. + * + * @param tier The tier. + * @param to Address to mint to. + * @param quantity Number of tokens to mint. + * @return fromTokenId The first token ID minted. + */ + function mint( + uint8 tier, + address to, + uint256 quantity + ) external payable returns (uint256 fromTokenId); + + /** + * @dev Mints `quantity` tokens to each of the addresses in `to`. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the + * `ADMIN_ROLE`, which can be granted via {grantRole}. + * + * @param tier The tier. + * @param to Address to mint to. + * @param quantity Number of tokens to mint. + * @return fromTokenId The first token ID minted. + */ + function airdrop( + uint8 tier, + address[] calldata to, + uint256 quantity + ) external payable returns (uint256 fromTokenId); + + /** + * @dev Withdraws collected ETH royalties to the fundingRecipient. + */ + function withdrawETH() external; + + /** + * @dev Withdraws collected ERC20 royalties to the fundingRecipient. + * @param tokens array of ERC20 tokens to withdraw + */ + function withdrawERC20(address[] calldata tokens) external; + + /** + * @dev Sets metadata module. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param metadataModule Address of metadata module. + */ + function setMetadataModule(address metadataModule) external; + + /** + * @dev Sets global base URI. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param baseURI The base URI to be set. + */ + function setBaseURI(string memory baseURI) external; + + /** + * @dev Sets contract URI. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param contractURI The contract URI to be set. + */ + function setContractURI(string memory contractURI) external; + + /** + * @dev Freezes metadata by preventing any more changes to base URI. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + */ + function freezeMetadata() external; + + /** + * @dev Freezes the max tier by preventing any more tiers from being added, + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + */ + function freezeCreateTier() external; + + /** + * @dev Sets funding recipient address. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param fundingRecipient Address to be set as the new funding recipient. + */ + function setFundingRecipient(address fundingRecipient) external; + + /** + * @dev Creates a new split wallet via the SplitMain contract, then sets it as the `fundingRecipient`. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param splitMain The address of the SplitMain contract. + * @param splitData The calldata to forward to the SplitMain contract to create a split. + * @return split The address of the new split contract. + */ + function createSplit(address splitMain, bytes calldata splitData) external returns (address split); + + /** + * @dev Sets royalty amount in bps (basis points). + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param bps The new royalty basis points to be set. + */ + function setRoyalty(uint16 bps) external; + + /** + * @dev Freezes the tier. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param tier The tier. + */ + function freezeTier(uint8 tier) external; + + /** + * @dev Sets the edition max mintable range. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param tier The tier. + * @param lower The lower limit of the maximum number of tokens that can be minted. + * @param upper The upper limit of the maximum number of tokens that can be minted. + */ + function setMaxMintableRange( + uint8 tier, + uint32 lower, + uint32 upper + ) external; + + /** + * @dev Sets the timestamp after which, the `editionMaxMintable` drops + * from `editionMaxMintableUpper` to `editionMaxMintableLower. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param tier The tier. + * @param cutoffTime The timestamp. + */ + function setCutoffTime(uint8 tier, uint32 cutoffTime) external; + + /** + * @dev Sets whether the `mintRandomness` is enabled. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param tier The tier. + * @param enabled The boolean value. + */ + function setMintRandomnessEnabled(uint8 tier, bool enabled) external; + + /** + * @dev Adds a new tier. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param creation The tier creation data. + */ + function createTier(TierCreation calldata creation) external; + + /** + * @dev Emits an event to signal to marketplaces to refresh all the metadata. + */ + function emitAllMetadataUpdate() external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the edition info. + * @return info The latest value. + */ + function editionInfo() external view returns (EditionInfo memory info); + + /** + * @dev Returns the tier info. + * @param tier The tier. + * @return info The latest value. + */ + function tierInfo(uint8 tier) external view returns (TierInfo memory info); + + /** + * @dev Returns the GA tier, which is 0. + * @return The constant value. + */ + function GA_TIER() external pure returns (uint8); + + /** + * @dev Basis points denominator used in fee calculations. + * @return The constant value. + */ + function BPS_DENOMINATOR() external pure returns (uint16); + + /** + * @dev Returns the minter role flag. + * Note: This constant will always be 2 for past and future sound protocol contracts. + * @return The constant value. + */ + function MINTER_ROLE() external view returns (uint256); + + /** + * @dev Returns the admin role flag. + * Note: This constant will always be 1 for past and future sound protocol contracts. + * @return The constant value. + */ + function ADMIN_ROLE() external view returns (uint256); + + /** + * @dev Returns the tier of the `tokenId`. + * @param tokenId The token ID. + * @return The latest value. + */ + function tokenTier(uint256 tokenId) external view returns (uint8); + + /** + * @dev Returns the tier of the `tokenId`. + * Note: Will NOT revert if any `tokenId` does not exist. + * If the token has not been minted, the tier will be zero. + * If the token is burned, the tier will be the tier before it was burned. + * @param tokenId The token ID. + * @return The latest value. + */ + function explicitTokenTier(uint256 tokenId) external view returns (uint8); + + /** + * @dev Returns the tiers of the `tokenIds`. + * Note: Will NOT revert if any `tokenId` does not exist. + * If the token has not been minted, the tier will be zero. + * If the token is burned, the tier will be the tier before it was burned. + * @param tokenIds The token IDs. + * @return The latest values. + */ + function tokenTiers(uint256[] calldata tokenIds) external view returns (uint8[] memory); + + /** + * @dev Returns an array of all the token IDs in the tier. + * @param tier The tier. + * @return tokenIds The array of token IDs in the tier. + */ + function tierTokenIds(uint8 tier) external view returns (uint256[] memory tokenIds); + + /** + * @dev Returns an array of all the token IDs in the tier, within the range [start, stop). + * @param tier The tier. + * @param start The start of the range. Inclusive. + * @param stop The end of the range. Exclusive. + * @return tokenIds The array of token IDs in the tier. + */ + function tierTokenIdsIn( + uint8 tier, + uint256 start, + uint256 stop + ) external view returns (uint256[] memory tokenIds); + + /** + * @dev Returns the index of `tokenId` in it's tier token ID array. + * @param tokenId The token ID to find. + * @return The index of `tokenId`. If not found, returns `type(uint256).max`. + */ + function tierTokenIdIndex(uint256 tokenId) external view returns (uint256); + + /** + * @dev Returns the maximum amount of tokens mintable for the tier. + * @param tier The tier. + * @return The configured value. + */ + function maxMintable(uint8 tier) external view returns (uint32); + + /** + * @dev Returns the upper bound for the maximum tokens that can be minted for the tier. + * @param tier The tier. + * @return The configured value. + */ + function maxMintableUpper(uint8 tier) external view returns (uint32); + + /** + * @dev Returns the lower bound for the maximum tokens that can be minted for the tier. + * @param tier The tier. + * @return The configured value. + */ + function maxMintableLower(uint8 tier) external view returns (uint32); + + /** + * @dev Returns the timestamp after which `maxMintable` drops from + * `maxMintableUpper` to `maxMintableLower`. + * @param tier The tier. + * @return The configured value. + */ + function cutoffTime(uint8 tier) external view returns (uint32); + + /** + * @dev Returns the number of tokens minted for the tier. + * @param tier The tier. + * @return The latest value. + */ + function tierMinted(uint8 tier) external view returns (uint32); + + /** + * @dev Returns the mint randomness for the tier. + * @param tier The tier. + * @return The latest value. + */ + function mintRandomness(uint8 tier) external view returns (uint256); + + /** + * @dev Returns the one-of-one token ID for the tier. + * @param tier The tier. + * @return The latest value. + */ + function mintRandomnessOneOfOne(uint8 tier) external view returns (uint32); + + /** + * @dev Returns whether the `mintRandomness` has been enabled. + * @return The configured value. + */ + function mintRandomnessEnabled(uint8 tier) external view returns (bool); + + /** + * @dev Returns whether the mint has been concluded for the tier. + * @param tier The tier. + * @return The latest value. + */ + function mintConcluded(uint8 tier) external view returns (bool); + + /** + * @dev Returns the base token URI for the collection. + * @return The configured value. + */ + function baseURI() external view returns (string memory); + + /** + * @dev Returns the contract URI to be used by Opensea. + * See: https://docs.opensea.io/docs/contract-level-metadata + * @return The configured value. + */ + function contractURI() external view returns (string memory); + + /** + * @dev Returns the address of the funding recipient. + * @return The configured value. + */ + function fundingRecipient() external view returns (address); + + /** + * @dev Returns the address of the metadata module. + * @return The configured value. + */ + function metadataModule() external view returns (address); + + /** + * @dev Returns the royalty basis points. + * @return The configured value. + */ + function royaltyBPS() external view returns (uint16); + + /** + * @dev Returns whether the tier is frozen. + * @return The configured value. + */ + function isFrozen(uint8 tier) external view returns (bool); + + /** + * @dev Returns whether the metadata module is frozen. + * @return The configured value. + */ + function isMetadataFrozen() external view returns (bool); + + /** + * @dev Returns whether the ability to create tiers is frozen. + * @return The configured value. + */ + function isCreateTierFrozen() external view returns (bool); + + /** + * @dev Returns the next token ID to be minted. + * @return The latest value. + */ + function nextTokenId() external view returns (uint256); + + /** + * @dev Returns the number of tokens minted by `owner`. + * @param owner Address to query for number minted. + * @return The latest value. + */ + function numberMinted(address owner) external view returns (uint256); + + /** + * @dev Returns the number of tokens burned by `owner`. + * @param owner Address to query for number burned. + * @return The latest value. + */ + function numberBurned(address owner) external view returns (uint256); + + /** + * @dev Returns the total amount of tokens minted. + * @return The latest value. + */ + function totalMinted() external view returns (uint256); + + /** + * @dev Returns the total amount of tokens burned. + * @return The latest value. + */ + function totalBurned() external view returns (uint256); + + /** + * @dev Returns the token URI of `tokenId`, but without reverting if + * the token does not exist. + * @return The latest value. + */ + function explicitTokenURI(uint256 tokenId) external view returns (string memory); + + /** + * @dev Informs other contracts which interfaces this contract supports. + * Required by https://eips.ethereum.org/EIPS/eip-165 + * @param interfaceId The interface id to check. + * @return Whether the `interfaceId` is supported. + */ + function supportsInterface(bytes4 interfaceId) + external + view + override(IERC721AUpgradeable, IERC165Upgradeable) + returns (bool); +} diff --git a/contracts/modules/SuperMinterV2.sol b/contracts/modules/SuperMinterV2.sol new file mode 100644 index 00000000..68921625 --- /dev/null +++ b/contracts/modules/SuperMinterV2.sol @@ -0,0 +1,1292 @@ +// 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 { ISuperMinterV2 } from "@modules/interfaces/ISuperMinterV2.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 SuperMinterV2 + * @dev The `SuperMinterV2` class is a generalized minter. + */ +contract SuperMinterV2 is ISuperMinterV2, 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 price per token. + uint96 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 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; + } + + // ============================================================= + // 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," + "uint96 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. + */ + uint96 public constant MAX_PER_MINT_REWARD = 0.1 ether; + + /** + * @dev The maximum platform per-transaction flat fee. + */ + uint96 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` => `feeRecipient`. + */ + mapping(address => address) public platformFeeAddress; + + /** + * @dev A mapping of `affiliate` => `feesAccrued`. + */ + mapping(address => uint256) public affiliateFeesAccrued; + + /** + * @dev A mapping of `platform` => `price`. + */ + mapping(address => uint96) 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 ISuperMinterV2 + */ + 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; + 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; + + emit MintCreated(c.edition, c.tier, scheduleNum, c); + } + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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); + + 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 uint96s. + unchecked { + if (l.finalAffiliateFee != 0) { + affiliateFeesAccrued[p.affiliate] += l.finalAffiliateFee; + } + if (l.finalPlatformFee != 0) { + platformFeesAccrued[d.platform] += l.finalPlatformFee; + } + } + + /* ------------------------- MINT --------------------------- */ + + ISoundEditionV2_1 edition = ISoundEditionV2_1(p.edition); + l.quantity = p.quantity; + l.fromTokenId = edition.mint{ value: l.finalArtistFee }(p.tier, p.to, p.quantity); + l.allowlisted = p.allowlisted; + l.allowlistedQuantity = p.allowlistedQuantity; + l.signedClaimTicket = p.signedClaimTicket; + l.requiredEtherValue = f.total; + l.unitPrice = f.unitPrice; + + emit Minted(p.edition, p.tier, p.scheduleNum, p.to, l, p.attributionId); + + return l.fromTokenId; + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function setPrice( + address edition, + uint8 tier, + uint8 scheduleNum, + uint96 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function withdrawForAffiliate(address affiliate) public { + uint256 accrued = affiliateFeesAccrued[affiliate]; + if (accrued != 0) { + affiliateFeesAccrued[affiliate] = 0; + SafeTransferLib.forceSafeTransferETH(affiliate, accrued); + emit AffiliateFeesWithdrawn(affiliate, accrued); + } + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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); + } + } + + // Platform fee functions: + // ----------------------- + // These functions enable any caller to set their own platform fees. + + /** + * @inheritdoc ISuperMinterV2 + */ + function setPlatformFeeAddress(address recipient) public { + address sender = LibMulticaller.senderOrSigner(); + _validatePlatformFeeAddress(recipient); + platformFeeAddress[sender] = recipient; + emit PlatformFeeAddressSet(sender, recipient); + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function setGAPrice(uint96 price) public { + address sender = LibMulticaller.senderOrSigner(); + gaPrice[sender] = price; + emit GAPriceSet(sender, price); + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function totalPriceAndFeesWithSignedPrice( + address edition, + uint8 tier, + uint8 scheduleNum, + uint32 quantity, + uint96 signedPrice, + bool hasValidAffiliate + ) public view returns (TotalPriceAndFees memory) { + uint256 mintId = LibOps.packId(edition, tier, scheduleNum); + return _totalPriceAndFees(tier, _getMintData(mintId), quantity, signedPrice, hasValidAffiliate); + } + + /** + * @inheritdoc ISuperMinterV2 + */ + function nextScheduleNum(address edition, uint8 tier) public view returns (uint8) { + return _mintData[LibOps.packId(edition, tier, 0)].nextScheduleNum; + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function isAffiliated( + address edition, + uint8 tier, + uint8 scheduleNum, + address affiliate + ) public view virtual returns (bool) { + return isAffiliatedWithProof(edition, tier, scheduleNum, affiliate, MerkleProofLib.emptyProof()); + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function platformFeeConfig(address platform, uint8 tier) public view returns (PlatformFeeConfig memory) { + return _platformFeeConfigs[LibOps.packId(platform, tier)]; + } + + /** + * @inheritdoc ISuperMinterV2 + */ + function defaultPlatformFeeConfig(address platform) public view returns (PlatformFeeConfig memory) { + return _platformFeeConfigs[LibOps.packId(platform, _DEFAULT_FEE_CONFIG_INDEX)]; + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + 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 ISuperMinterV2 + */ + function name() external pure returns (string memory name_) { + (name_, ) = _domainNameAndVersion(); + } + + /** + * @inheritdoc ISuperMinterV2 + */ + 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(ISuperMinterV2).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_ = "SuperMinter"; + version_ = "1_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, + uint96 signedPrice, + bool hasValidAffiliate + ) internal view returns (TotalPriceAndFees memory f) { + // All flat prices are stored as uint96s in storage. + // The quantity is a uint32. Multiplications between a uint96 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/ISuperMinterV2.sol b/contracts/modules/interfaces/ISuperMinterV2.sol new file mode 100644 index 00000000..7189ef00 --- /dev/null +++ b/contracts/modules/interfaces/ISuperMinterV2.sol @@ -0,0 +1,1028 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; + +/** + * @title ISuperMinterV2 + * @notice The interface for the generalized minter. + */ +interface ISuperMinterV2 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`. + uint96 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; + } + + /** + * @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`. + uint96 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 requiredEtherValue; + // 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 to hold the fee configuration for a platform and a tier. + */ + struct PlatformFeeConfig { + // The amount of reward to give to the artist per mint. + uint96 artistMintReward; + // The amount of reward to give to the affiliate per mint. + uint96 affiliateMintReward; + // The amount of reward to give to the platform per mint. + uint96 platformMintReward; + // If the price is greater than this, the rewards will become the threshold variants. + uint96 thresholdPrice; + // The amount of reward to give to the artist (`unitPrice >= thresholdPrice`). + uint96 thresholdArtistMintReward; + // The amount of reward to give to the affiliate (`unitPrice >= thresholdPrice`). + uint96 thresholdAffiliateMintReward; + // The amount of reward to give to the platform (`unitPrice >= thresholdPrice`). + uint96 thresholdPlatformMintReward; + // The per-transaction flat fee. + uint96 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. + uint96 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, uint96 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 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 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, uint96 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(); + + // ============================================================= + // 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, + uint96 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 fees of the affiliate, to the affiliate. + * @param affiliate The affiliate address. + */ + function withdrawForAffiliate(address affiliate) external; + + /** + * @dev Withdraws all accrued fees of the platform, to the their fee address. + * @param platform The platform address. + */ + function withdrawForPlatform(address platform) 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(uint96 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 (uint96); + + /** + * @dev The maximum platform per-transaction flat fee. + * @return The constant value. + */ + function MAX_PLATFORM_PER_TX_FLAT_FEE() external pure returns (uint96); + + /** + * @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, + uint96 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 (uint96); + + /** + * @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/multicaller b/lib/multicaller index d77e75ac..8c071078 160000 --- a/lib/multicaller +++ b/lib/multicaller @@ -1 +1 @@ -Subproject commit d77e75acbea633a986d8150f0b117895e7934299 +Subproject commit 8c071078b29d0037a7f01ec6f346776ec7c89948 diff --git a/lib/solady b/lib/solady index 77809c18..cde0a5fb 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 77809c18e010b914dde9518956a4ae7cb507d383 +Subproject commit cde0a5fb594da8655ba6bfcdc2e40a7c870c0cc0 diff --git a/tests/TestConfigV2_1.sol b/tests/TestConfigV2_1.sol new file mode 100644 index 00000000..1f22c1a1 --- /dev/null +++ b/tests/TestConfigV2_1.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import "./TestPlus.sol"; +import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; +import { ISoundEditionV2_1, SoundEditionV2_1 } from "@core/SoundEditionV2_1.sol"; + +contract TestConfigV2_1 is TestPlus { + uint256 internal _salt; + + SoundCreatorV1 soundCreator; + + function setUp() public virtual { + soundCreator = new SoundCreatorV1(address(new SoundEditionV2_1())); + } + + function createSoundEdition(ISoundEditionV2_1.EditionInitialization memory init) public returns (SoundEditionV2_1) { + bytes memory initData = abi.encodeWithSelector(SoundEditionV2_1.initialize.selector, init); + + address[] memory contracts; + bytes[] memory data; + + soundCreator.createSoundAndMints(bytes32(++_salt), initData, contracts, data); + (address addr, ) = soundCreator.soundEditionAddress(address(this), bytes32(_salt)); + return SoundEditionV2_1(addr); + } + + function genericEditionInitialization() public view returns (ISoundEditionV2_1.EditionInitialization memory init) { + init.fundingRecipient = address(this); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](1); + } +} diff --git a/tests/core/SoundEditionV2_1.t.sol b/tests/core/SoundEditionV2_1.t.sol new file mode 100644 index 00000000..1c24a847 --- /dev/null +++ b/tests/core/SoundEditionV2_1.t.sol @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC721AUpgradeable, ISoundEditionV2_1, SoundEditionV2_1 } from "@core/SoundEditionV2_1.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import { LibMap } from "solady/utils/LibMap.sol"; +import "../TestConfigV2_1.sol"; + +contract SoundEditionV2_1Tests is TestConfigV2_1 { + using LibMap for *; + + event MetadataModuleSet(address metadataModule); + event BaseURISet(string baseURI); + event ContractURISet(string contractURI); + event MetadataFrozen(address metadataModule, string baseURI, string contractURI); + event CreateTierFrozen(); + event FundingRecipientSet(address recipient); + event RoyaltySet(uint16 bps); + event MaxMintableRangeSet(uint8 tier, uint32 lower, uint32 upper); + event CutoffTimeSet(uint8 tier, uint32 cutoff); + event MintRandomnessEnabledSet(uint8 tier, bool enabled); + event SoundEditionInitialized(ISoundEditionV2_1.EditionInitialization init); + event TierCreated(ISoundEditionV2_1.TierCreation creation); + event TierFrozen(uint8 tier); + event ETHWithdrawn(address recipient, uint256 amount, address caller); + event ERC20Withdrawn(address recipient, address[] tokens, uint256[] amounts, address caller); + event Minted(uint8 tier, address to, uint256 quantity, uint256 fromTokenId, uint32 fromTierTokenIdIndex); + event Airdropped(uint8 tier, address[] to, uint256 quantity, uint256 fromTokenId, uint32 fromTierTokenIdIndex); + event BatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId); + + uint16 public constant BPS_DENOMINATOR = 10000; + + function test_initialization(uint256) public { + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + + init.royaltyBPS = uint16(_bound(_random(), 0, BPS_DENOMINATOR)); + init.isCreateTierFrozen = _random() % 2 == 0; + init.isMetadataFrozen = _random() % 2 == 0; + init.metadataModule = _randomNonZeroAddress(); + init.tierCreations[0].tier = uint8(_bound(_random(), 1, 255)); + init.tierCreations[0].maxMintableLower = uint32(_random() % 255); + init.tierCreations[0].maxMintableUpper = 255 | uint32(_random() % 10); + + vm.expectEmit(true, true, true, true); + emit SoundEditionInitialized(init); + edition = createSoundEdition(init); + + ISoundEditionV2_1.EditionInfo memory info = edition.editionInfo(); + assertEq(info.royaltyBPS, init.royaltyBPS); + assertEq(info.isCreateTierFrozen, init.isCreateTierFrozen); + assertEq(info.isMetadataFrozen, init.isMetadataFrozen); + assertEq(info.metadataModule, init.metadataModule); + } + + function test_initializationReverts() public { + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + + createSoundEdition(init); + + init = genericEditionInitialization(); + init.fundingRecipient = address(0); + vm.expectRevert(ISoundEditionV2_1.InvalidFundingRecipient.selector); + createSoundEdition(init); + + init = genericEditionInitialization(); + init.royaltyBPS = BPS_DENOMINATOR + 1; + vm.expectRevert(ISoundEditionV2_1.InvalidRoyaltyBPS.selector); + createSoundEdition(init); + + init = genericEditionInitialization(); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](0); + vm.expectRevert(ISoundEditionV2_1.ZeroTiersProvided.selector); + createSoundEdition(init); + + init = genericEditionInitialization(); + init.tierCreations[0].tier = 1; + init.tierCreations[0].maxMintableUpper = 0; + init.tierCreations[0].maxMintableLower = 1; + vm.expectRevert(ISoundEditionV2_1.InvalidMaxMintableRange.selector); + createSoundEdition(init); + } + + function test_tierTokenIds(uint256) public { + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init; + init.fundingRecipient = address(this); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](3); + for (uint256 i; i < 3; ++i) { + init.tierCreations[i].tier = uint8(i); + init.tierCreations[i].maxMintableLower = 10; + init.tierCreations[i].maxMintableUpper = 10; + init.tierCreations[i].cutoffTime = uint32(block.timestamp + 60); + init.tierCreations[i].mintRandomnessEnabled = true; + } + + edition = createSoundEdition(init); + + uint256[] memory tokenIds = edition.tierTokenIds(0); + + _mintOrAirdrop(edition, 0, address(this), 2); // 1, 2. + _mintOrAirdrop(edition, 1, address(this), 3); // 3, 4, 5. + _mintOrAirdrop(edition, 2, address(this), 3); // 6, 7, 8. + _mintOrAirdrop(edition, 0, address(this), 1); // 9. + _mintOrAirdrop(edition, 2, address(this), 1); // 10. + + tokenIds = edition.tierTokenIds(0); + assertEq(tokenIds.length, 3); + assertEq(tokenIds[0], 1); + assertEq(tokenIds[1], 2); + assertEq(tokenIds[2], 9); + assertEq(edition.tokenTier(1), 0); + assertEq(edition.tokenTier(2), 0); + assertEq(edition.tokenTier(9), 0); + + tokenIds = edition.tierTokenIds(1); + assertEq(tokenIds.length, 3); + assertEq(tokenIds[0], 3); + assertEq(tokenIds[1], 4); + assertEq(tokenIds[2], 5); + assertEq(edition.tokenTier(3), 1); + assertEq(edition.tokenTier(4), 1); + assertEq(edition.tokenTier(5), 1); + + tokenIds = edition.tierTokenIds(2); + assertEq(tokenIds.length, 4); + assertEq(tokenIds[0], 6); + assertEq(tokenIds[1], 7); + assertEq(tokenIds[2], 8); + assertEq(tokenIds[3], 10); + assertEq(edition.tokenTier(6), 2); + assertEq(edition.tokenTier(7), 2); + assertEq(edition.tokenTier(8), 2); + assertEq(edition.tokenTier(10), 2); + } + + function _mintOrAirdrop( + ISoundEditionV2_1 edition, + uint8 tier, + address to, + uint256 quantity + ) internal { + uint256 r = _random() % 3; + uint256 expectedFromTokenId = edition.nextTokenId(); + uint32 expectedFromTierTokenIdIndex = edition.tierInfo(tier).minted; + if (r == 0) { + vm.expectEmit(true, true, true, true); + emit Minted(tier, to, quantity, expectedFromTokenId, expectedFromTierTokenIdIndex); + edition.mint(tier, to, quantity); + } else if (r == 1) { + address[] memory recipients = new address[](quantity); + for (uint256 i; i != quantity; ++i) { + recipients[i] = to; + } + vm.expectEmit(true, true, true, true); + emit Airdropped(tier, recipients, 1, expectedFromTokenId, expectedFromTierTokenIdIndex); + edition.airdrop(tier, recipients, 1); + } else { + address[] memory recipients = new address[](1); + recipients[0] = to; + vm.expectEmit(true, true, true, true); + emit Airdropped(tier, recipients, quantity, expectedFromTokenId, expectedFromTierTokenIdIndex); + edition.airdrop(tier, recipients, quantity); + } + } + + function test_mintsForNonGA(uint256) public { + uint8 tier = uint8(_bound(_random(), 1, 255)); + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init; + init.fundingRecipient = address(this); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](1); + init.tierCreations[0].tier = tier; + init.tierCreations[0].maxMintableLower = 5; + init.tierCreations[0].maxMintableUpper = 10; + init.tierCreations[0].cutoffTime = uint32(block.timestamp + 60); + init.tierCreations[0].mintRandomnessEnabled = true; + + edition = createSoundEdition(init); + + vm.expectRevert(IERC721AUpgradeable.MintZeroQuantity.selector); + edition.mint(tier, address(this), 0); + + bool testCutoffTime = _random() % 2 == 0; + uint32 limit = init.tierCreations[0].maxMintableUpper; + if (testCutoffTime) { + limit = init.tierCreations[0].maxMintableLower; + vm.warp(init.tierCreations[0].cutoffTime); + } + assertEq(edition.editionInfo().tierInfo[0].maxMintable, limit); + + uint32 remainder = uint32(_bound(_random(), 1, limit - 1)); + _checkMints(edition, tier, true, false); + edition.mint(tier, address(this), limit - remainder); + _checkMints(edition, tier, true, false); + + vm.expectRevert(ISoundEditionV2_1.ExceedsAvailableSupply.selector); + edition.mint(tier, address(this), remainder + 1); + + edition.mint(tier, address(this), remainder); + _checkMints(edition, tier, true, true); + + vm.expectRevert(ISoundEditionV2_1.ExceedsAvailableSupply.selector); + edition.mint(tier, address(this), 1); + + _checkMints(edition, tier, true, true); + } + + function _checkMints( + SoundEditionV2_1 edition, + uint8 tier, + bool hasMintRandomness, + bool mintConcluded + ) internal { + assertEq(edition.mintConcluded(tier), mintConcluded); + assertEq(edition.mintRandomness(tier) != 0, hasMintRandomness && mintConcluded); + uint32 oneOfOne = edition.mintRandomnessOneOfOne(tier); + assertEq(oneOfOne != 0, hasMintRandomness && mintConcluded); + } + + function test_updateGATier() public { + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init; + init.fundingRecipient = address(this); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](1); + init.tierCreations[0].tier = 0; + edition = createSoundEdition(init); + + assertEq(edition.editionInfo().tierInfo[0].isFrozen, true); + assertEq(edition.isFrozen(0), true); + + vm.expectRevert(ISoundEditionV2_1.TierIsFrozen.selector); + edition.setMaxMintableRange(0, 7, 11); + + vm.expectRevert(ISoundEditionV2_1.TierIsFrozen.selector); + edition.freezeTier(0); + + vm.expectRevert(ISoundEditionV2_1.TierIsFrozen.selector); + edition.setMintRandomnessEnabled(0, false); + } + + function test_updateNonGATier() public { + uint8 tier = uint8(_bound(_random(), 1, 255)); + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init; + init.fundingRecipient = address(this); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](1); + init.tierCreations[0].tier = tier; + init.tierCreations[0].maxMintableLower = 5; + init.tierCreations[0].maxMintableUpper = 10; + init.tierCreations[0].cutoffTime = uint32(block.timestamp + 60); + init.tierCreations[0].mintRandomnessEnabled = true; + + edition = createSoundEdition(init); + + edition.setCutoffTime(tier, uint32(block.timestamp + 100)); + assertEq(edition.editionInfo().tierInfo[0].cutoffTime, uint32(block.timestamp + 100)); + assertEq(edition.cutoffTime(tier), uint32(block.timestamp + 100)); + + vm.expectEmit(true, true, true, true); + emit MaxMintableRangeSet(tier, 7, 11); + edition.setMaxMintableRange(tier, 7, 11); + assertEq(edition.editionInfo().tierInfo[0].maxMintableLower, 7); + assertEq(edition.maxMintableLower(tier), 7); + assertEq(edition.editionInfo().tierInfo[0].maxMintableUpper, 11); + assertEq(edition.maxMintableUpper(tier), 11); + + vm.expectEmit(true, true, true, true); + emit MintRandomnessEnabledSet(tier, false); + edition.setMintRandomnessEnabled(tier, false); + assertEq(edition.editionInfo().tierInfo[0].mintRandomnessEnabled, false); + assertEq(edition.mintRandomnessEnabled(tier), false); + + vm.expectEmit(true, true, true, true); + emit MintRandomnessEnabledSet(tier, true); + edition.setMintRandomnessEnabled(tier, true); + assertEq(edition.editionInfo().tierInfo[0].mintRandomnessEnabled, true); + assertEq(edition.mintRandomnessEnabled(tier), true); + + assertEq(edition.editionInfo().tierInfo[0].isFrozen, false); + assertEq(edition.isFrozen(tier), false); + vm.expectEmit(true, true, true, true); + emit TierFrozen(tier); + edition.freezeTier(tier); + assertEq(edition.editionInfo().tierInfo[0].isFrozen, true); + assertEq(edition.isFrozen(tier), true); + + vm.expectRevert(ISoundEditionV2_1.TierIsFrozen.selector); + edition.setCutoffTime(tier, uint32(block.timestamp + 100)); + + vm.expectRevert(ISoundEditionV2_1.TierIsFrozen.selector); + edition.setMaxMintableRange(tier, 7, 11); + + vm.expectRevert(ISoundEditionV2_1.TierIsFrozen.selector); + edition.setMintRandomnessEnabled(tier, false); + } + + function test_createTiers(uint256) public { + uint8[] memory uniqueTiers = _uniqueTiers(true); + + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init; + init.fundingRecipient = address(this); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](uniqueTiers.length); + for (uint256 i; i < uniqueTiers.length; ++i) { + ISoundEditionV2_1.TierCreation memory tierCreation = init.tierCreations[i]; + tierCreation.tier = uniqueTiers[i]; + tierCreation.maxMintableLower = uint32(5 + i); + tierCreation.maxMintableUpper = uint32(10 + i); + tierCreation.cutoffTime = uint32(block.timestamp + 60 + i); + tierCreation.mintRandomnessEnabled = i % 2 == 0; + } + edition = createSoundEdition(init); + + for (uint256 i; i < uniqueTiers.length; ++i) { + edition.mint(uniqueTiers[i], address(this), i % 3 != 0 ? 10 + i : 1); + } + + ISoundEditionV2_1.EditionInfo memory info = edition.editionInfo(); + assertEq(info.tierInfo.length, uniqueTiers.length); + for (uint256 i; i < uniqueTiers.length; ++i) { + ISoundEditionV2_1.TierInfo memory tierInfo = info.tierInfo[i]; + assertEq(tierInfo.tier, uniqueTiers[i]); + if (tierInfo.tier == 0) { + assertEq(tierInfo.maxMintableLower, type(uint32).max); + assertEq(tierInfo.maxMintableUpper, type(uint32).max); + assertEq(tierInfo.cutoffTime, type(uint32).max); + assertEq(tierInfo.mintRandomnessEnabled, false); + assertEq(tierInfo.mintRandomness, 0); + } else { + assertEq(tierInfo.maxMintableLower, 5 + i); + assertEq(tierInfo.maxMintableUpper, 10 + i); + assertEq(tierInfo.cutoffTime, uint32(block.timestamp + 60 + i)); + assertEq(tierInfo.mintRandomnessEnabled, i % 2 == 0); + assertEq(tierInfo.mintConcluded, i % 3 != 0); + assertEq(tierInfo.mintRandomness != 0, tierInfo.mintConcluded && tierInfo.mintRandomnessEnabled); + } + } + + if (_random() % 2 == 0) { + ISoundEditionV2_1.TierCreation memory c; + c.tier = _newUniqueTier(uniqueTiers, false); + c.maxMintableLower = 55; + c.maxMintableUpper = 111; + c.cutoffTime = uint32(block.timestamp + 222); + + if (_random() % 2 == 0) { + vm.expectEmit(true, true, true, true); + emit TierCreated(c); + edition.createTier(c); + info = edition.editionInfo(); + assertEq(info.tierInfo.length, uniqueTiers.length + 1); + ISoundEditionV2_1.TierInfo memory tierInfo = info.tierInfo[uniqueTiers.length]; + assertEq(tierInfo.maxMintableLower, 55); + assertEq(tierInfo.maxMintableUpper, 111); + assertEq(tierInfo.cutoffTime, uint32(block.timestamp + 222)); + assertEq(tierInfo.mintRandomnessEnabled, false); + assertEq(tierInfo.mintConcluded, false); + assertEq(tierInfo.mintRandomness, 0); + } else { + assertEq(edition.editionInfo().isCreateTierFrozen, false); + edition.freezeCreateTier(); + assertEq(edition.editionInfo().isCreateTierFrozen, true); + vm.expectRevert(ISoundEditionV2_1.CreateTierIsFrozen.selector); + edition.createTier(c); + vm.expectRevert(ISoundEditionV2_1.CreateTierIsFrozen.selector); + edition.freezeCreateTier(); + } + } + } + + function _newUniqueTier(uint8[] memory uniqueTiers, bool includeGA) internal returns (uint8) { + unchecked { + while (true) { + uint256 r = _bound(_random(), includeGA ? 0 : 1, 255); + uint256 n = uniqueTiers.length; + bool found; + for (uint256 i; i != n; ++i) { + if (uniqueTiers[i] == r) found = true; + } + if (!found) return uint8(r); + } + return 0; + } + } + + function _uniqueTiers(bool includeGA) internal returns (uint8[] memory result) { + unchecked { + uint256 n = 1 + (_random() % 8); + uint256[] memory a = new uint256[](n); + for (uint256 i; i != n; ++i) { + a[i] = _bound(_random(), includeGA ? 0 : 1, 255); + } + LibSort.insertionSort(a); + LibSort.uniquifySorted(a); + assembly { + result := a + } + } + } + + function testMintRandomness(uint256) public { + SoundEditionV2_1 edition; + uint8 tier = uint8(_bound(_random(), 1, 255)); + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + if (_random() % 2 == 0) { + init.tierCreations[0].tier = tier; + init.tierCreations[0].maxMintableLower = 0; + init.tierCreations[0].maxMintableUpper = 0; + init.tierCreations[0].mintRandomnessEnabled = true; + edition = createSoundEdition(init); + assertEq(edition.mintConcluded(tier), true); + assertEq(edition.mintRandomness(tier) != 0, true); + assertEq(edition.mintRandomnessOneOfOne(tier) != 0, false); + } else { + uint32 limit = uint32(_bound(_random(), 1, 5)); + init.tierCreations[0].tier = tier; + init.tierCreations[0].maxMintableLower = limit; + init.tierCreations[0].maxMintableUpper = limit; + init.tierCreations[0].mintRandomnessEnabled = true; + edition = createSoundEdition(init); + assertEq(edition.mintConcluded(tier), false); + assertEq(edition.mintRandomness(tier) != 0, false); + assertEq(edition.mintRandomnessOneOfOne(tier) != 0, false); + _mintOrAirdrop(edition, tier, address(this), limit); + assertEq(edition.mintConcluded(tier), true); + assertEq(edition.mintRandomness(tier) != 0, true); + assertEq(edition.mintRandomnessOneOfOne(tier) != 0, true); + } + + vm.expectRevert(ISoundEditionV2_1.ExceedsAvailableSupply.selector); + edition.mint(tier, address(this), 1); + } + + function test_supportsInterface() public { + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + edition = createSoundEdition(init); + + assertTrue(edition.supportsInterface(0x80ac58cd)); // IERC721. + assertTrue(edition.supportsInterface(0x01ffc9a7)); // IERC165. + assertTrue(edition.supportsInterface(0x5b5e139f)); // IERC721Metadata. + assertTrue(edition.supportsInterface(type(ISoundEditionV2_1).interfaceId)); + + assertFalse(edition.supportsInterface(0x11223344)); // Some random ID. + } + + // ============================================================= + // SPLITS + // ============================================================= + + address splitWalletImplementation; + address splitMain; + + struct SplitData { + address[] accounts; + uint32[] percentAllocations; + uint32 distributorFee; + address controller; + } + + function _splitPercentageScale() internal returns (uint256) { + (bool success, bytes memory results) = splitMain.call(abi.encodeWithSignature("PERCENTAGE_SCALE()")); + assertTrue(success); + return abi.decode(results, (uint256)); + } + + function _randomSplitData() internal returns (SplitData memory data) { + uint256 percentageScale = _splitPercentageScale(); + + data.accounts = new address[](2); + (data.accounts[0], ) = _randomSigner(); + (data.accounts[1], ) = _randomSigner(); + LibSort.insertionSort(data.accounts); + + data.percentAllocations = new uint32[](2); + data.percentAllocations[0] = uint32(percentageScale / 2); + data.percentAllocations[1] = uint32(percentageScale - data.percentAllocations[0]); + + data.distributorFee = 0; + + (data.controller, ) = _randomSigner(); + } + + function _encodeCreateSplitData(SplitData memory data) internal pure returns (bytes memory) { + return + abi.encodeWithSignature( + "createSplit(address[],uint32[],uint32,address)", + data.accounts, + data.percentAllocations, + data.distributorFee, + data.controller + ); + } + + function _checkSplit(SoundEditionV2_1 edition) internal { + address split = edition.fundingRecipient(); + (bool success, bytes memory results) = split.call(abi.encodeWithSignature("splitMain()")); + assertTrue(success); + assertEq(abi.decode(results, (address)), splitMain); + } + + function _deploySplitContracts() internal { + splitWalletImplementation = 0xD94c0CE4f8eEfA4Ebf44bf6665688EdEEf213B33; + splitMain = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + vm.etch( + splitMain, + hex"6080604052600436106101185760003560e01c806377b1e4e9116100a0578063c7de644011610064578063c7de64401461034e578063d0e4b2f41461036e578063e10e51d61461038e578063e61cb05e146103cb578063ecef0ace146103eb57600080fd5b806377b1e4e91461027e5780638117abc11461029e57806388c662aa146102d2578063a5e3909e1461030e578063c3a8962c1461032e57600080fd5b80633bb66a7b116100e75780633bb66a7b146101cf5780633f26479e146101ef57806352844dd3146102065780636e5f69191461023e5780637601f7821461025e57600080fd5b80631267c6da146101245780631581130214610146578063189cbaa0146101665780631da0b8fc1461018657600080fd5b3661011f57005b600080fd5b34801561013057600080fd5b5061014461013f366004612ab2565b61040b565b005b34801561015257600080fd5b50610144610161366004612c4c565b6104a6565b34801561017257600080fd5b50610144610181366004612ab2565b61081a565b34801561019257600080fd5b506101bc6101a1366004612ab2565b6001600160a01b031660009081526002602052604090205490565b6040519081526020015b60405180910390f35b3480156101db57600080fd5b506101bc6101ea366004612ab2565b6108e5565b3480156101fb57600080fd5b506101bc620f424081565b34801561021257600080fd5b50610226610221366004612d5d565b61093e565b6040516001600160a01b0390911681526020016101c6565b34801561024a57600080fd5b50610144610259366004612d03565b610c4d565b34801561026a57600080fd5b50610226610279366004612ddb565b610d82565b34801561028a57600080fd5b50610144610299366004612c4c565b611144565b3480156102aa57600080fd5b506102267f000000000000000000000000d94c0ce4f8eefa4ebf44bf6665688edeef213b3381565b3480156102de57600080fd5b506102266102ed366004612ab2565b6001600160a01b039081166000908152600260205260409020600101541690565b34801561031a57600080fd5b50610144610329366004612b95565b611487565b34801561033a57600080fd5b506101bc610349366004612c3a565b6117aa565b34801561035a57600080fd5b50610144610369366004612ab2565b61187e565b34801561037a57600080fd5b50610144610389366004612ace565b61194d565b34801561039a57600080fd5b506102266103a9366004612ab2565b6001600160a01b03908116600090815260026020819052604090912001541690565b3480156103d757600080fd5b506101446103e6366004612b95565b611a1f565b3480156103f757600080fd5b50610144610406366004612b06565b611d6f565b6001600160a01b0381811660009081526002602052604090206001015482911633146104515760405163472511eb60e11b81523360048201526024015b60405180910390fd5b6001600160a01b038216600081815260026020819052604080832090910180546001600160a01b0319169055517f6c2460a415b84be3720c209fe02f2cad7a6bcba21e8637afe8957b7ec4b6ef879190a25050565b85858080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808902828101820190935288825290935088925087918291850190849080828437600092019190915250508351869250600211159050610535578251604051630e8c626560e41b815260040161044891815260200190565b8151835114610564578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f424061057183612020565b63ffffffff16146105a75761058582612020565b60405163fcc487c160e01b815263ffffffff9091166004820152602401610448565b82516000190160005b8181101561069e578481600101815181106105db57634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b031685828151811061060c57634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b03161061063e5760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff1684828151811061066657634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff16141561069657604051630db7e4c760e01b815260048101829052602401610448565b6001016105b0565b50600063ffffffff168382815181106106c757634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff1614156106f757604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff16111561072a5760405163308440e360e21b815263ffffffff82166004820152602401610448565b61079a8b8a8a8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808e0282810182019093528d82529093508d92508c9182918501908490808284376000920191909152508b9250612073915050565b61080d8b8b8b8b8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808f0282810182019093528e82529093508e92508d9182918501908490808284376000920191909152508c92508b91506120c59050565b5050505050505050505050565b6001600160a01b03818116600090815260026020526040902060010154829116331461085b5760405163472511eb60e11b8152336004820152602401610448565b6001600160a01b03808316600081815260026020819052604080832091820180546001600160a01b0319169055600190910154905191931691907f943d69cf2bbe08a9d44b3c4ce6da17d939d758739370620871ce99a6437866d0908490a4506001600160a01b0316600090815260026020526040902060010180546001600160a01b0319169055565b6001600160a01b038116600090815260026020526040812054610909576000610915565b816001600160a01b0316315b6001600160a01b0383166000908152602081905260409020546109389190612f98565b92915050565b6000858580806020026020016040519081016040528093929190818152602001838360200280828437600092019190915250506040805160208089028281018201909352888252909350889250879182918501908490808284376000920191909152505083518692506002111590506109cf578251604051630e8c626560e41b815260040161044891815260200190565b81518351146109fe578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f4240610a0b83612020565b63ffffffff1614610a1f5761058582612020565b82516000190160005b81811015610b1657848160010181518110610a5357634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b0316858281518110610a8457634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b031610610ab65760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff16848281518110610ade57634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415610b0e57604051630db7e4c760e01b815260048101829052602401610448565b600101610a28565b50600063ffffffff16838281518110610b3f57634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415610b6f57604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff161115610ba25760405163308440e360e21b815263ffffffff82166004820152602401610448565b6000610c138a8a8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808e0282810182019093528d82529093508d92508c9182918501908490808284376000920191909152508b925061239f915050565b9050610c3f7f000000000000000000000000d94c0ce4f8eefa4ebf44bf6665688edeef213b33826123d5565b9a9950505050505050505050565b60008167ffffffffffffffff811115610c7657634e487b7160e01b600052604160045260246000fd5b604051908082528060200260200182016040528015610c9f578160200160208202803683370190505b50905060008415610cb657610cb38661247a565b90505b60005b83811015610d3257610cff87868684818110610ce557634e487b7160e01b600052603260045260246000fd5b9050602002016020810190610cfa9190612ab2565b6124cd565b838281518110610d1f57634e487b7160e01b600052603260045260246000fd5b6020908102919091010152600101610cb9565b50856001600160a01b03167fa9e30bf144f83390a4fe47562a4e16892108102221c674ff538da0b72a83d17482868686604051610d729493929190612f08565b60405180910390a2505050505050565b600086868080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808a02828101820190935289825290935089925088918291850190849080828437600092019190915250508351879250600211159050610e13578251604051630e8c626560e41b815260040161044891815260200190565b8151835114610e42578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f4240610e4f83612020565b63ffffffff1614610e635761058582612020565b82516000190160005b81811015610f5a57848160010181518110610e9757634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b0316858281518110610ec857634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b031610610efa5760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff16848281518110610f2257634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415610f5257604051630db7e4c760e01b815260048101829052602401610448565b600101610e6c565b50600063ffffffff16838281518110610f8357634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415610fb357604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff161115610fe65760405163308440e360e21b815263ffffffff82166004820152602401610448565b60006110578b8b8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808f0282810182019093528e82529093508e92508d9182918501908490808284376000920191909152508c925061239f915050565b90506001600160a01b038616611098576110917f000000000000000000000000d94c0ce4f8eefa4ebf44bf6665688edeef213b3382612539565b94506110f5565b6110c17f000000000000000000000000d94c0ce4f8eefa4ebf44bf6665688edeef213b336125e9565b6001600160a01b03818116600090815260026020526040902060010180546001600160a01b03191691891691909117905594505b6001600160a01b038516600081815260026020526040808220849055517f8d5f9943c664a3edaf4d3eb18cc5e2c45a7d2dc5869be33d33bbc0fff9bc25909190a2505050509695505050505050565b6001600160a01b0388811660009081526002602052604090206001015489911633146111855760405163472511eb60e11b8152336004820152602401610448565b86868080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808a02828101820190935289825290935089925088918291850190849080828437600092019190915250508351879250600211159050611214578251604051630e8c626560e41b815260040161044891815260200190565b8151835114611243578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f424061125083612020565b63ffffffff16146112645761058582612020565b82516000190160005b8181101561135b5784816001018151811061129857634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b03168582815181106112c957634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b0316106112fb5760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff1684828151811061132357634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff16141561135357604051630db7e4c760e01b815260048101829052602401610448565b60010161126d565b50600063ffffffff1683828151811061138457634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff1614156113b457604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff1611156113e75760405163308440e360e21b815263ffffffff82166004820152602401610448565b6113f58c8b8b8b8b8b612698565b6114798c8c8c8c80806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f820116905080830192505050505050508b8b808060200260200160405190810160405280939291908181526020018383602002808284376000920191909152508d92508c91506120c59050565b505050505050505050505050565b6001600160a01b0387811660009081526002602052604090206001015488911633146114c85760405163472511eb60e11b8152336004820152602401610448565b86868080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808a02828101820190935289825290935089925088918291850190849080828437600092019190915250508351879250600211159050611557578251604051630e8c626560e41b815260040161044891815260200190565b8151835114611586578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f424061159383612020565b63ffffffff16146115a75761058582612020565b82516000190160005b8181101561169e578481600101815181106115db57634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b031685828151811061160c57634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b03161061163e5760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff1684828151811061166657634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff16141561169657604051630db7e4c760e01b815260048101829052602401610448565b6001016115b0565b50600063ffffffff168382815181106116c757634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff1614156116f757604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff16111561172a5760405163308440e360e21b815263ffffffff82166004820152602401610448565b6117388b8b8b8b8b8b612698565b61080d8b8b8b8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808f0282810182019093528e82529093508e92508d9182918501908490808284376000920191909152508c92508b91506127589050565b6001600160a01b0382166000908152600260205260408120546117ce576000611847565b6040516370a0823160e01b81526001600160a01b0384811660048301528316906370a082319060240160206040518083038186803b15801561180f57600080fd5b505afa158015611823573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906118479190612e6c565b6001600160a01b038084166000908152600160209081526040808320938816835292905220546118779190612f98565b9392505050565b6001600160a01b038181166000908152600260208190526040909120015482911633146118c05760405163472511eb60e11b8152336004820152602401610448565b6001600160a01b03808316600081815260026020819052604080832091820180546001600160a01b0319169055600190910154905133949190911692917f943d69cf2bbe08a9d44b3c4ce6da17d939d758739370620871ce99a6437866d091a4506001600160a01b0316600090815260026020526040902060010180546001600160a01b03191633179055565b6001600160a01b03828116600090815260026020526040902060010154839116331461198e5760405163472511eb60e11b8152336004820152602401610448565b816001600160a01b0381166119c15760405163c369130760e01b81526001600160a01b0382166004820152602401610448565b6001600160a01b03848116600081815260026020819052604080832090910180546001600160a01b0319169488169485179055517f107cf6ea8668d533df1aab5bb8b6315bb0c25f0b6c955558d09368f290668fc79190a350505050565b85858080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808902828101820190935288825290935088925087918291850190849080828437600092019190915250508351869250600211159050611aae578251604051630e8c626560e41b815260040161044891815260200190565b8151835114611add578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f4240611aea83612020565b63ffffffff1614611afe5761058582612020565b82516000190160005b81811015611bf557848160010181518110611b3257634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b0316858281518110611b6357634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b031610611b955760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff16848281518110611bbd57634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415611bed57604051630db7e4c760e01b815260048101829052602401610448565b600101611b07565b50600063ffffffff16838281518110611c1e57634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415611c4e57604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff161115611c815760405163308440e360e21b815263ffffffff82166004820152602401610448565b611cf18a8a8a8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808e0282810182019093528d82529093508d92508c9182918501908490808284376000920191909152508b9250612073915050565b611d638a8a8a8080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808e0282810182019093528d82529093508d92508c9182918501908490808284376000920191909152508b92508a91506127589050565b50505050505050505050565b6001600160a01b038681166000908152600260205260409020600101548791163314611db05760405163472511eb60e11b8152336004820152602401610448565b85858080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808902828101820190935288825290935088925087918291850190849080828437600092019190915250508351869250600211159050611e3f578251604051630e8c626560e41b815260040161044891815260200190565b8151835114611e6e578251825160405163b34f351d60e01b815260048101929092526024820152604401610448565b620f4240611e7b83612020565b63ffffffff1614611e8f5761058582612020565b82516000190160005b81811015611f8657848160010181518110611ec357634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b0316858281518110611ef457634e487b7160e01b600052603260045260246000fd5b60200260200101516001600160a01b031610611f265760405163ac6bd23360e01b815260048101829052602401610448565b600063ffffffff16848281518110611f4e57634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415611f7e57604051630db7e4c760e01b815260048101829052602401610448565b600101611e98565b50600063ffffffff16838281518110611faf57634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff161415611fdf57604051630db7e4c760e01b815260048101829052602401610448565b50620186a08163ffffffff1611156120125760405163308440e360e21b815263ffffffff82166004820152602401610448565b611d638a8a8a8a8a8a612698565b8051600090815b8181101561206c5783818151811061204f57634e487b7160e01b600052603260045260246000fd5b6020026020010151836120629190612fb0565b9250600101612027565b5050919050565b600061208084848461239f565b6001600160a01b03861660009081526002602052604090205490915081146120be5760405163dd5ff45760e01b815260048101829052602401610448565b5050505050565b6001600160a01b038581166000818152600160209081526040808320948b16808452949091528082205490516370a0823160e01b815260048101949094529092909183916370a082319060240160206040518083038186803b15801561212a57600080fd5b505afa15801561213e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906121629190612e6c565b9050801561216f57600019015b811561217c576001820391505b818101925081156121b0576001600160a01b038089166000908152600160208181526040808420948e168452939052919020555b836001600160a01b0316886001600160a01b03168a6001600160a01b03167fb5ee5dc3d2c31a019bbf2c787e0e9c97971c96aceea1c38c12fc8fd25c536d46866040516121ff91815260200190565b60405180910390a463ffffffff851615612271576001600160a01b038881166000908152600160205260408120620f424063ffffffff891687020492839290881661224a573361224c565b875b6001600160a01b03168152602081019190915260400160002080549091019055909203915b865160005b81811015612329576122ba858983815181106122a257634e487b7160e01b600052603260045260246000fd5b602002602001015163ffffffff16620f424091020490565b6001600160a01b038b1660009081526001602052604081208b519091908c90859081106122f757634e487b7160e01b600052603260045260246000fd5b6020908102919091018101516001600160a01b0316825281019190915260400160002080549091019055600101612276565b5050801561239457604051633e0f9fff60e11b81526001600160a01b038981166004830152602482018390528a1690637c1f3ffe90604401600060405180830381600087803b15801561237b57600080fd5b505af115801561238f573d6000803e3d6000fd5b505050505b505050505050505050565b60008383836040516020016123b693929190612e84565b6040516020818303038152906040528051906020012090509392505050565b6000611877838330604051723d605d80600a3d3981f336603057343d52307f60681b81527f830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b160138201527260203da23d3df35b3d3d3d3d363d3d37363d7360681b6033820152606093841b60468201526d5af43d3d93803e605b57fd5bf3ff60901b605a820152921b6068830152607c8201526067808220609c830152605591012090565b6001600160a01b03811660009081526020819052604081205461249f90600190612fd8565b6001600160a01b0383166000818152602081905260409020600190559091506124c8908261293a565b919050565b6001600160a01b038082166000908152600160208181526040808420948716845293905291812054909161250091612fd8565b6001600160a01b038084166000818152600160208181526040808420958a16845294905292902091909155909150610938908483612990565b6000604051723d605d80600a3d3981f336603057343d52307f60681b81527f830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b160138201527260203da23d3df35b3d3d3d3d363d3d37363d7360681b60338201528360601b60468201526c5af43d3d93803e605b57fd5bf360981b605a820152826067826000f59150506001600160a01b0381166109385760405163380bbe1360e01b815260040160405180910390fd5b6000604051723d605d80600a3d3981f336603057343d52307f60681b81527f830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b160138201527260203da23d3df35b3d3d3d3d363d3d37363d7360681b60338201528260601b60468201526c5af43d3d93803e605b57fd5bf360981b605a8201526067816000f09150506001600160a01b0381166124c857604051630985da9b60e41b815260040160405180910390fd5b600061270986868080602002602001604051908101604052809392919081815260200183836020028082843760009201919091525050604080516020808a0282810182019093528982529093508992508891829185019084908082843760009201919091525087925061239f915050565b6001600160a01b0388166000818152600260205260408082208490555192935090917f45e1e99513dd915ac128b94953ca64c6375717ea1894b3114db08cdca51debd29190a250505050505050565b6001600160a01b0385166000818152602081905260408120549131908215612781576001830392505b5081810182156127a8576001600160a01b0388166000908152602081905260409020600190555b836001600160a01b0316886001600160a01b03167f87c3ca0a87d9b82033e4bc55e6d30621f8d7e0c9d8ca7988edfde8932787b77b836040516127ed91815260200190565b60405180910390a363ffffffff85161561284c57620f424063ffffffff8616820204806000806001600160a01b0388166128275733612829565b875b6001600160a01b0316815260208101919091526040016000208054909101905590035b865160005b818110156128d25761287d838983815181106122a257634e487b7160e01b600052603260045260246000fd5b6000808b84815181106128a057634e487b7160e01b600052603260045260246000fd5b6020908102919091018101516001600160a01b0316825281019190915260400160002080549091019055600101612851565b5050811561293057604051632ac3affd60e21b8152600481018390526001600160a01b0389169063ab0ebff490602401600060405180830381600087803b15801561291c57600080fd5b505af1158015611479573d6000803e3d6000fd5b5050505050505050565b600080600080600085875af190508061298b5760405162461bcd60e51b815260206004820152601360248201527211551217d514905394d1915497d19052531151606a1b6044820152606401610448565b505050565b600060405163a9059cbb60e01b81526001600160a01b03841660048201528260248201526000806044836000895af19150506129cb81612a0f565b612a095760405162461bcd60e51b815260206004820152600f60248201526e1514905394d1915497d19052531151608a1b6044820152606401610448565b50505050565b60003d82612a2157806000803e806000fd5b8060208114612a39578015612a4a576000925061206c565b816000803e6000511515925061206c565b5060019392505050565b60008083601f840112612a65578182fd5b50813567ffffffffffffffff811115612a7c578182fd5b6020830191508360208260051b8501011115612a9757600080fd5b9250929050565b803563ffffffff811681146124c857600080fd5b600060208284031215612ac3578081fd5b813561187781613005565b60008060408385031215612ae0578081fd5b8235612aeb81613005565b91506020830135612afb81613005565b809150509250929050565b60008060008060008060808789031215612b1e578182fd5b8635612b2981613005565b9550602087013567ffffffffffffffff80821115612b45578384fd5b612b518a838b01612a54565b90975095506040890135915080821115612b69578384fd5b50612b7689828a01612a54565b9094509250612b89905060608801612a9e565b90509295509295509295565b600080600080600080600060a0888a031215612baf578081fd5b8735612bba81613005565b9650602088013567ffffffffffffffff80821115612bd6578283fd5b612be28b838c01612a54565b909850965060408a0135915080821115612bfa578283fd5b50612c078a828b01612a54565b9095509350612c1a905060608901612a9e565b91506080880135612c2a81613005565b8091505092959891949750929550565b60008060408385031215612ae0578182fd5b60008060008060008060008060c0898b031215612c67578081fd5b8835612c7281613005565b97506020890135612c8281613005565b9650604089013567ffffffffffffffff80821115612c9e578283fd5b612caa8c838d01612a54565b909850965060608b0135915080821115612cc2578283fd5b50612ccf8b828c01612a54565b9095509350612ce2905060808a01612a9e565b915060a0890135612cf281613005565b809150509295985092959890939650565b60008060008060608587031215612d18578384fd5b8435612d2381613005565b935060208501359250604085013567ffffffffffffffff811115612d45578283fd5b612d5187828801612a54565b95989497509550505050565b600080600080600060608688031215612d74578081fd5b853567ffffffffffffffff80821115612d8b578283fd5b612d9789838a01612a54565b90975095506020880135915080821115612daf578283fd5b50612dbc88828901612a54565b9094509250612dcf905060408701612a9e565b90509295509295909350565b60008060008060008060808789031215612df3578182fd5b863567ffffffffffffffff80821115612e0a578384fd5b612e168a838b01612a54565b90985096506020890135915080821115612e2e578384fd5b50612e3b89828a01612a54565b9095509350612e4e905060408801612a9e565b91506060870135612e5e81613005565b809150509295509295509295565b600060208284031215612e7d578081fd5b5051919050565b835160009082906020808801845b83811015612eb75781516001600160a01b031685529382019390820190600101612e92565b50508651818801939250845b81811015612ee557845163ffffffff1684529382019392820192600101612ec3565b50505060e09490941b6001600160e01b0319168452505060049091019392505050565b84815260606020808301829052908201849052600090859060808401835b87811015612f54578335612f3981613005565b6001600160a01b031682529282019290820190600101612f26565b5084810360408601528551808252908201925081860190845b81811015612f8957825185529383019391830191600101612f6d565b50929998505050505050505050565b60008219821115612fab57612fab612fef565b500190565b600063ffffffff808316818516808303821115612fcf57612fcf612fef565b01949350505050565b600082821015612fea57612fea612fef565b500390565b634e487b7160e01b600052601160045260246000fd5b6001600160a01b038116811461301a57600080fd5b5056fea264697066735822122078638564d8f0338df6cf15b5c2680d5c2ef45167f59938471977e9756316b94964736f6c63430008040033" + ); + vm.etch( + splitWalletImplementation, + hex"6080604052600436106100345760003560e01c80630e769b2b146100395780637c1f3ffe14610089578063ab0ebff41461009e575b600080fd5b34801561004557600080fd5b5061006d7f0000000000000000000000002ed6c4b5da6378c7897ac67ba9e43102feb694ee81565b6040516001600160a01b03909116815260200160405180910390f35b61009c6100973660046102d0565b6100b1565b005b61009c6100ac366004610306565b610131565b336001600160a01b037f0000000000000000000000002ed6c4b5da6378c7897ac67ba9e43102feb694ee16146100f9576040516282b42960e81b815260040160405180910390fd5b61012d6001600160a01b0383167f0000000000000000000000002ed6c4b5da6378c7897ac67ba9e43102feb694ee836101af565b5050565b336001600160a01b037f0000000000000000000000002ed6c4b5da6378c7897ac67ba9e43102feb694ee1614610179576040516282b42960e81b815260040160405180910390fd5b6101ac6001600160a01b037f0000000000000000000000002ed6c4b5da6378c7897ac67ba9e43102feb694ee1682610233565b50565b600060405163a9059cbb60e01b81526001600160a01b03841660048201528260248201526000806044836000895af19150506101ea81610289565b61022d5760405162461bcd60e51b815260206004820152600f60248201526e1514905394d1915497d19052531151608a1b60448201526064015b60405180910390fd5b50505050565b600080600080600085875af19050806102845760405162461bcd60e51b815260206004820152601360248201527211551217d514905394d1915497d19052531151606a1b6044820152606401610224565b505050565b60003d8261029b57806000803e806000fd5b80602081146102b35780156102c457600092506102c9565b816000803e600051151592506102c9565b600192505b5050919050565b600080604083850312156102e2578182fd5b82356001600160a01b03811681146102f8578283fd5b946020939093013593505050565b600060208284031215610317578081fd5b503591905056fea26469706673582212208e095a368bcb2efb2a8afd9b560ad94441e926086a4bc92edf16c33900df798e64736f6c63430008040033" + ); + + bool success; + bytes memory results; + + (success, results) = splitMain.call(abi.encodeWithSignature("walletImplementation()")); + assertTrue(success); + assertEq(abi.decode(results, (address)), splitWalletImplementation); + + (success, results) = splitWalletImplementation.call(abi.encodeWithSignature("splitMain()")); + assertTrue(success); + assertEq(abi.decode(results, (address)), splitMain); + } + + function test_setSplit() public { + _deploySplitContracts(); + + SoundEditionV2_1 edition; + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + edition = createSoundEdition(init); + + SplitData memory splitData = _randomSplitData(); + edition.createSplit(splitMain, _encodeCreateSplitData(splitData)); + _checkSplit(edition); + } +} diff --git a/tests/modules/SuperMinterV2.t.sol b/tests/modules/SuperMinterV2.t.sol new file mode 100644 index 00000000..55f78d57 --- /dev/null +++ b/tests/modules/SuperMinterV2.t.sol @@ -0,0 +1,915 @@ +pragma solidity ^0.8.16; + +import { Merkle } from "murky/Merkle.sol"; +import { IERC721AUpgradeable, ISoundEditionV2_1, SoundEditionV2_1 } from "@core/SoundEditionV2_1.sol"; +import { ISuperMinterV2, SuperMinterV2 } from "@modules/SuperMinterV2.sol"; +import { DelegateCashLib } from "@modules/utils/DelegateCashLib.sol"; +import { LibOps } from "@core/utils/LibOps.sol"; +import { Ownable } from "solady/auth/Ownable.sol"; +import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import "../TestConfigV2_1.sol"; + +contract SuperMinterV2Tests is TestConfigV2_1 { + SuperMinterV2 sm; + SoundEditionV2_1 edition; + Merkle merkle; + + event Minted( + address indexed edition, + uint8 tier, + uint8 scheduleNum, + address indexed to, + ISuperMinterV2.MintedLogData data, + uint256 indexed attributionId + ); + + struct SuperMinterV2Constants { + uint96 MAX_PLATFORM_PER_TX_FLAT_FEE; + uint96 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 SuperMinterV2(); + edition.grantRoles(address(sm), edition.MINTER_ROLE()); + merkle = new Merkle(); + } + + function _superMinterConstants() internal view returns (SuperMinterV2Constants 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(uint96(gaPrice)); + + assertEq(sm.mintInfoList(address(edition)).length, 0); + for (uint256 j; j < 3; ++j) { + for (uint256 i; i < 3; ++i) { + ISuperMinterV2.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.tier = uint8(i * 2); + c.price = uint96(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); + + ISuperMinterV2.MintInfo[] memory mintInfoList = sm.mintInfoList(address(edition)); + assertEq(mintInfoList.length, 3 * 3); + for (uint256 j; j < 3; ++j) { + for (uint256 i; i < 3; ++i) { + ISuperMinterV2.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 { + ISuperMinterV2.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 = uint96(_bound(_random(), 0, type(uint96).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); + + ISuperMinterV2.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(ISuperMinterV2.NotConfigurable.selector); + sm.setMerkleRoot(address(edition), c.tier, 0, c.merkleRoot); + + assertEq(info.price, 0); + vm.expectRevert(ISuperMinterV2.NotConfigurable.selector); + sm.setPrice(address(edition), c.tier, 0, c.price); + + assertEq(info.maxMintable, type(uint32).max); + vm.expectRevert(ISuperMinterV2.NotConfigurable.selector); + sm.setMaxMintable(address(edition), c.tier, 0, c.maxMintable); + + assertEq(info.maxMintablePerAccount, type(uint32).max); + vm.expectRevert(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.NotConfigurable.selector); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } + } else { + if (c.mode == sm.DEFAULT()) { + assertEq(info.merkleRoot, bytes32(0)); + vm.expectRevert(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.NotConfigurable.selector); + sm.setMaxMintablePerAccount(address(edition), c.tier, 0, c.maxMintablePerAccount); + } + } + } + + function test_platformAirdrop(uint256) public { + (address signer, uint256 privateKey) = _randomSigner(); + + ISuperMinterV2.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 { + ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.SignatureAlreadyUsed.selector); + sm.platformAirdrop(p); + } + } + + function test_mintDefaultUpToMaxPerAccount() public { + ISuperMinterV2.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); + + ISuperMinterV2.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(ISuperMinterV2.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])); + + ISuperMinterV2.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); + + ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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])); + + ISuperMinterV2.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); + + ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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 { + SuperMinterV2Constants memory smc = _superMinterConstants(); + uint96 perTxFlat = uint96(_bound(_random(), 0, smc.MAX_PLATFORM_PER_TX_FLAT_FEE * 2)); + uint96 platformReward = uint96(_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(ISuperMinterV2.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( + uint96 perTxFlat, + uint96 platformReward, + uint16 perMintBPS, + bool active + ) internal { + ISuperMinterV2.PlatformFeeConfig memory c; + c.platformTxFlatFee = perTxFlat; + c.platformMintReward = platformReward; + c.platformMintFeeBPS = perMintBPS; + c.active = active; + sm.setDefaultPlatformFeeConfig(c); + } + + function _setPlatformFeeConfig( + uint8 tier, + uint96 perTxFlat, + uint96 platformReward, + uint16 perMintBPS, + bool active + ) internal { + ISuperMinterV2.PlatformFeeConfig memory c; + c.platformTxFlatFee = perTxFlat; + c.platformMintReward = platformReward; + c.platformMintFeeBPS = perMintBPS; + c.active = active; + sm.setPlatformFeeConfig(tier, c); + } + + function _checkDefaultPlatformFeeConfig( + uint96 perTxFlat, + uint96 platformReward, + uint16 perMintBPS, + bool active + ) internal { + _checkPlatformFeeConfig( + sm.defaultPlatformFeeConfig(address(this)), + perTxFlat, + platformReward, + perMintBPS, + active + ); + } + + function _checkEffectivePlatformFeeConfig( + uint8 tier, + uint96 perTxFlat, + uint96 platformReward, + uint16 perMintBPS, + bool active + ) internal { + _checkPlatformFeeConfig( + sm.effectivePlatformFeeConfig(address(this), tier), + perTxFlat, + platformReward, + perMintBPS, + active + ); + } + + function _checkPlatformFeeConfig( + ISuperMinterV2.PlatformFeeConfig memory result, + uint96 perTxFlat, + uint96 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 { + SuperMinterV2Constants memory smc = _superMinterConstants(); + + ISuperMinterV2.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 = uint96(_bound(_random(), 0, type(uint96).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 = uint96(_bound(_random(), 0, type(uint96).max)); + vm.prank(c.platform); + sm.setGAPrice(uint96(gaPrice)); + + uint32 quantity = uint32(_bound(_random(), 1, type(uint32).max)); + uint96 signedPrice = uint96(_bound(_random(), 1, type(uint96).max)); + ISuperMinterV2.TotalPriceAndFees memory tpaf; + if (c.mode == sm.VERIFY_SIGNATURE() && signedPrice < c.price) { + vm.expectRevert(ISuperMinterV2.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); + } + + ISuperMinterV2.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 { + SuperMinterV2Constants memory smc = _superMinterConstants(); + address[] memory feeRecipients = _twoRandomUniqueAddresses(); + + // Create a tier 1 mint schedule, without any affiliate root. + ISuperMinterV2.MintCreation memory c; + { + c.maxMintable = type(uint32).max; + c.platform = _randomNonZeroAddress(); + c.edition = address(edition); + c.tier = 1; + c.price = uint96(_bound(_random(), 0, type(uint96).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. + ISuperMinterV2.PlatformFeeConfig memory pfc; + { + pfc.platformTxFlatFee = uint96(_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 = uint96(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.affiliateMintReward = uint96(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.platformMintReward = uint96(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + + pfc.thresholdPrice = uint96(_bound(_random(), 0, type(uint96).max)); + + pfc.thresholdArtistMintReward = uint96(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.thresholdAffiliateMintReward = uint96(_bound(_random(), 0, smc.MAX_PER_MINT_REWARD)); + pfc.thresholdPlatformMintReward = uint96(_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. + ISuperMinterV2.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); + + ISuperMinterV2.MintedLogData memory l; + ISuperMinterV2.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.requiredEtherValue = 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.requiredEtherValue = 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_mintWithSignature(uint256) public { + (address signer, uint256 privateKey) = _randomSigner(); + ISuperMinterV2.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); + + ISuperMinterV2.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 = uint96(_bound(_random(), 0, type(uint96).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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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(ISuperMinterV2.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 { + ISuperMinterV2.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 = uint96(_bound(_random(), 0, type(uint96).max)); + vm.prank(c.platform); + sm.setGAPrice(uint96(gaPrice)); + + ISuperMinterV2.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); + } +}