From b56a0fb027a73e821b546375f71939d70af07023 Mon Sep 17 00:00:00 2001 From: Vignesh Hirudayakanth Date: Mon, 3 Jul 2023 14:49:44 -0400 Subject: [PATCH] Vigneshka/minters 2 1 (#290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add platformPerTxFlatFee * Rename files and add tests * Rename platformFlatFee -> platformFlatFeeTotal * Add platform fee info to mint info struct * Add SAMV1_1 * Add missing platformPerTxFlatFee to ISAMV1_1 * generate sam id * changeset * remove recursive publish * temp * Refactor totalPrice to totalPriceAndFees * Add test to ensure no overflow --------- Co-authored-by: Vectorized Co-authored-by: Pablo Sáez --- .changeset/famous-games-peel.md | 5 + .../core/interfaces/IMinterModuleV2_1.sol | 495 +++++++++ contracts/core/utils/MintRandomnessLib.sol | 5 +- contracts/modules/BaseMinterV2_1.sol | 585 +++++++++++ contracts/modules/EditionMaxMinterV2_1.sol | 165 +++ .../modules/FixedPriceSignatureMinterV2_1.sol | 279 ++++++ contracts/modules/MerkleDropMinterV2_1.sol | 248 +++++ contracts/modules/MinterAdapter.sol | 2 +- contracts/modules/RangeEditionMinterV2_1.sol | 288 ++++++ contracts/modules/SAMV1_1.sol | 945 ++++++++++++++++++ .../interfaces/IEditionMaxMinterV2_1.sol | 183 ++++ .../IFixedPriceSignatureMinterV2_1.sol | 264 +++++ .../interfaces/IMerkleDropMinterV2_1.sol | 283 ++++++ .../modules/interfaces/IMinterAdapter.sol | 4 +- .../interfaces/IRangeEditionMinterV2_1.sol | 265 +++++ contracts/modules/interfaces/ISAMV1_1.sol | 807 +++++++++++++++ package.json | 2 +- script/solidity/Deploy.1.2.0.s.sol | 16 +- script/solidity/GetInterfaceId.s.sol | 39 +- src/interfaceIds.ts | 7 + src/json/interfaceIds.json | 15 +- tests/core/SoundCreator.t.sol | 18 +- ...l => RangeEditionMinterV2_1Invariants.sol} | 24 +- .../{MockMinterV2.sol => MockMinterV2_1.sol} | 27 +- ...aseMinterV2.t.sol => BaseMinterV2_1.t.sol} | 112 ++- ...terV2.t.sol => EditionMaxMinterV2_1.t.sol} | 76 +- ...ol => FixedPriceSignatureMinterV2_1.t.sol} | 105 +- tests/modules/GoldenEggMetadata.t.sol | 24 +- ...terV2.t.sol => MerkleDropMinterV2_1.t.sol} | 64 +- tests/modules/MintersIntegrationV2.t.sol | 14 +- tests/modules/OpenGoldenEggMetadata.t.sol | 74 +- ...rV2.t.sol => RangeEditionMinterV2_1.t.sol} | 108 +- tests/modules/{SAM.t.sol => SAMV1_1.t.sol} | 85 +- 33 files changed, 5284 insertions(+), 349 deletions(-) create mode 100644 .changeset/famous-games-peel.md create mode 100644 contracts/core/interfaces/IMinterModuleV2_1.sol create mode 100644 contracts/modules/BaseMinterV2_1.sol create mode 100644 contracts/modules/EditionMaxMinterV2_1.sol create mode 100644 contracts/modules/FixedPriceSignatureMinterV2_1.sol create mode 100644 contracts/modules/MerkleDropMinterV2_1.sol create mode 100644 contracts/modules/RangeEditionMinterV2_1.sol create mode 100644 contracts/modules/SAMV1_1.sol create mode 100644 contracts/modules/interfaces/IEditionMaxMinterV2_1.sol create mode 100644 contracts/modules/interfaces/IFixedPriceSignatureMinterV2_1.sol create mode 100644 contracts/modules/interfaces/IMerkleDropMinterV2_1.sol create mode 100644 contracts/modules/interfaces/IRangeEditionMinterV2_1.sol create mode 100644 contracts/modules/interfaces/ISAMV1_1.sol rename tests/invariants/{RangeEditionMinterV2Invariants.sol => RangeEditionMinterV2_1Invariants.sol} (73%) rename tests/mocks/{MockMinterV2.sol => MockMinterV2_1.sol} (75%) rename tests/modules/{BaseMinterV2.t.sol => BaseMinterV2_1.t.sol} (82%) rename tests/modules/{EditionMaxMinterV2.t.sol => EditionMaxMinterV2_1.t.sol} (77%) rename tests/modules/{FixedPriceSignatureMinterV2.t.sol => FixedPriceSignatureMinterV2_1.t.sol} (82%) rename tests/modules/{MerkleDropMinterV2.t.sol => MerkleDropMinterV2_1.t.sol} (75%) rename tests/modules/{RangeEditionMinterV2.t.sol => RangeEditionMinterV2_1.t.sol} (77%) rename tests/modules/{SAM.t.sol => SAMV1_1.t.sol} (96%) diff --git a/.changeset/famous-games-peel.md b/.changeset/famous-games-peel.md new file mode 100644 index 00000000..149a8a22 --- /dev/null +++ b/.changeset/famous-games-peel.md @@ -0,0 +1,5 @@ +--- +"@soundxyz/sound-protocol": patch +--- + +Platform flat transaction fee support diff --git a/contracts/core/interfaces/IMinterModuleV2_1.sol b/contracts/core/interfaces/IMinterModuleV2_1.sol new file mode 100644 index 00000000..67715b5b --- /dev/null +++ b/contracts/core/interfaces/IMinterModuleV2_1.sol @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; + +/** + * @title IMinterModuleV2_1 + * @notice The interface for Sound protocol minter modules. + */ +interface IMinterModuleV2_1 is IERC165 { + // ============================================================= + // STRUCTS + // ============================================================= + + struct BaseData { + // Auxillary variable for storing the price. + // May or may not be used. + uint96 price; + // Auxillary variable for storing the max amount mintable by an account. + // May or may not be used. + uint32 maxMintablePerAccount; + // The start unix timestamp of the mint. + uint32 startTime; + // The end unix timestamp of the mint. + uint32 endTime; + // The affiliate fee in basis points. + uint16 affiliateFeeBPS; + // Whether the mint is paused. + bool mintPaused; + // Whether the mint has been created. + bool created; + // The Merkle root of the affiliate allow list, if any. + bytes32 affiliateMerkleRoot; + } + + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when the mint instance for an `edition` is created. + * @param edition The edition address. + * @param mintId The mint ID, a global incrementing identifier used within the minter + * @param startTime The start time of the mint. + * @param endTime The end time of the mint. + * @param affiliateFeeBPS The affiliate fee in basis points. + */ + event MintConfigCreated( + address indexed edition, + address indexed creator, + uint128 mintId, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS + ); + + /** + * @dev Emitted when the `paused` status of `edition` is updated. + * @param edition The edition address. + * @param mintId The mint ID, to distinguish between multiple mints for the same edition. + * @param paused The new paused status. + */ + event MintPausedSet(address indexed edition, uint128 mintId, bool paused); + + /** + * @dev Emitted when the `paused` status of `edition` is updated. + * @param edition The edition address. + * @param mintId The mint ID, to distinguish between multiple mints for the same edition. + * @param startTime The start time of the mint. + * @param endTime The end time of the mint. + */ + event TimeRangeSet(address indexed edition, uint128 mintId, uint32 startTime, uint32 endTime); + + /** + * @notice Emitted when the `affiliateFeeBPS` is updated. + * @param edition The edition address. + * @param mintId The mint ID, to distinguish between multiple mints for the same edition. + * @param bps The affiliate fee basis points. + */ + event AffiliateFeeSet(address indexed edition, uint128 mintId, uint16 bps); + + /** + * @dev Emitted when the Merkle root for an affiliate allow list is set. + * @param edition The edition address. + * @param mintId The mint ID, to distinguish between multiple mints for the same edition. + * @param root The Merkle root for the affiliate allow list. + */ + event AffiliateMerkleRootSet(address indexed edition, uint128 mintId, bytes32 root); + + /** + * @notice Emitted when a mint happens. + * @param edition The edition address. + * @param mintId The mint ID, to distinguish between multiple mints for + * the same edition. + * @param buyer The buyer address. + * @param fromTokenId The first token ID of the batch. + * @param quantity The size of the batch. + * @param requiredEtherValue Total amount of Ether required for payment. + * @param platformFee The cut paid to the platform. + * @param affiliateFee The cut paid to the affiliate. + * @param affiliate The affiliate's address. + * @param affiliated Whether the affiliate is affiliated. + * @param attributionId The attribution ID. + */ + event Minted( + address indexed edition, + uint128 mintId, + address indexed buyer, + uint32 fromTokenId, + uint32 quantity, + uint128 requiredEtherValue, + uint128 platformFee, + uint128 affiliateFee, + address affiliate, + bool affiliated, + uint256 indexed attributionId + ); + + /** + * @dev Emitted when the `platformFeeBPS` is updated. + * @param bps The platform fee basis points. + */ + event PlatformFeeSet(uint16 bps); + + /** + * @dev Emitted when the `platformFlatFee` is updated. + * @param flatFee The amount of platform flat fee per token. + */ + event PlatformFlatFeeSet(uint96 flatFee); + + /** + * @dev Emitted when the `platformPerTxFlatFee` is updated. + * @param perTxFlatFee The amount of platform flat fee per transaction. + */ + event PlatformPerTxFlatFeeSet(uint96 perTxFlatFee); + + /** + * @dev Emitted when the `platformFeeAddress` is updated. + * @param addr The platform fee address. + */ + event PlatformFeeAddressSet(address addr); + + /** + * @dev Emitted when the accrued fees for `affiliate` are withdrawn. + * @param affiliate The affiliate address. + * @param accrued The amount of fees withdrawn. + */ + event AffiliateFeesWithdrawn(address indexed affiliate, uint256 accrued); + + /** + * @dev Emitted when the accrued fees for the platform are withdrawn. + * @param accrued The amount of fees withdrawn. + */ + event PlatformFeesWithdrawn(uint128 accrued); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The Ether value paid is below the value required. + * @param paid The amount sent to the contract. + * @param required The amount required to mint. + */ + error Underpaid(uint256 paid, uint256 required); + + /** + * @dev The Ether value paid is not exact. + * @param paid The amount sent to the contract. + * @param required The amount required to mint. + */ + error WrongPayment(uint256 paid, uint256 required); + + /** + * @dev The number minted has exceeded the max mintable amount. + * @param available The number of tokens remaining available for mint. + */ + error ExceedsAvailableSupply(uint32 available); + + /** + * @dev The mint is not opened. + * @param blockTimestamp The current block timestamp. + * @param startTime The start time of the mint. + * @param endTime The end time of the mint. + */ + error MintNotOpen(uint256 blockTimestamp, uint32 startTime, uint32 endTime); + + /** + * @dev The mint is paused. + */ + error MintPaused(); + + /** + * @dev The `startTime` is not less than the `endTime`. + */ + error InvalidTimeRange(); + + /** + * @dev The affiliate fee BPS must not exceed `MAX_AFFILIATE_FEE_BPS`. + */ + error InvalidAffiliateFeeBPS(); + + /** + * @dev The platform fee BPS must not exceed `MAX_PLATFORM_FEE_BPS`. + */ + error InvalidPlatformFeeBPS(); + + /** + * @dev The platform flat fee must not exceed `MAX_PLATFORM_FLAT_FEE`. + */ + error InvalidPlatformFlatFee(); + + /** + * @dev The platform per-transaction flat fee must not exceed `MAX_PLATFORM_PER_TX_FLAT_FEE`. + */ + error InvalidPlatformPerTxFlatFee(); + + /** + * @dev The platform fee address cannot be zero. + */ + error PlatformFeeAddressIsZero(); + + /** + * @dev The mint does not exist. + */ + error MintDoesNotExist(); + + /** + * @dev The `affiliate` provided is invalid for the given `affiliateProof`. + */ + error InvalidAffiliate(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Sets the paused status for (`edition`, `mintId`). + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + */ + function setEditionMintPaused( + address edition, + uint128 mintId, + bool paused + ) external; + + /** + * @dev Sets the time range for an edition mint. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param mintId The mint ID, a global incrementing identifier used within the minter + * @param startTime The start time of the mint. + * @param endTime The end time of the mint. + */ + function setTimeRange( + address edition, + uint128 mintId, + uint32 startTime, + uint32 endTime + ) external; + + /** + * @dev Sets the affiliate fee for (`edition`, `mintId`). + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param mintId The mint ID, a global incrementing identifier used within the minter + * @param affiliateFeeBPS The affiliate fee in basis points. + */ + function setAffiliateFee( + address edition, + uint128 mintId, + uint16 affiliateFeeBPS + ) external; + + /** + * @dev Sets the affiliate Merkle root for (`edition`, `mintId`). + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param mintId The mint ID, a global incrementing identifier used within the minter + * @param root The affiliate Merkle root, if any. + */ + function setAffiliateMerkleRoot( + address edition, + uint128 mintId, + bytes32 root + ) external; + + /** + * @dev Sets the platform fee bps. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param bps The platform fee in basis points. + */ + function setPlatformFee(uint16 bps) external; + + /** + * @dev Sets the platform flat fee. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param flatFee The platform flat fee. + */ + function setPlatformFlatFee(uint96 flatFee) external; + + /** + * @dev Sets the per-transaction platform flat fee. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param perTxFlatFee The platform per transaction flat fee. + */ + function setPlatformPerTxFlatFee(uint96 perTxFlatFee) external; + + /** + * @dev Sets the platform fee address. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param addr The platform fee address. + */ + function setPlatformFeeAddress(address addr) external; + + /** + * @dev Withdraws all the accrued fees for `affiliate`. + */ + function withdrawForAffiliate(address affiliate) external; + + /** + * @dev Withdraws all the accrued fees for the platform. + */ + function withdrawForPlatform() external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev This is the denominator, in basis points (BPS), for any of the fees. + * @return The constant value. + */ + function BPS_DENOMINATOR() external pure returns (uint16); + + /** + * @dev The maximum basis points (BPS) limit allowed for the affiliate fees. + * @return The constant value. + */ + function MAX_AFFILIATE_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum basis points (BPS) limit allowed for the platform fees. + * @return The constant value. + */ + function MAX_PLATFORM_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum value for platform flat fee per NFT. + * @return The constant value. + */ + function MAX_PLATFORM_FLAT_FEE() external pure returns (uint96); + + /** + * @dev The maximum value for platform flat fee per transaction. + * @return The constant value. + */ + function MAX_PLATFORM_PER_TX_FLAT_FEE() external pure returns (uint96); + + /** + * @dev The total fees accrued for `affiliate`. + * @param affiliate The affiliate's address. + * @return The latest value. + */ + function affiliateFeesAccrued(address affiliate) external view returns (uint128); + + /** + * @dev The total fees accrued for the platform. + * @return The latest value. + */ + function platformFeesAccrued() external view returns (uint128); + + /** + * @dev Whether `affiliate` is affiliated for (`edition`, `mintId`). + * @param edition The edition's address. + * @param mintId The mint ID. + * @param affiliate The affiliate's address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @return The computed value. + */ + function isAffiliatedWithProof( + address edition, + uint128 mintId, + address affiliate, + bytes32[] calldata affiliateProof + ) external view returns (bool); + + /** + * @dev Whether `affiliate` is affiliated for (`edition`, `mintId`). + * @param edition The edition's address. + * @param mintId The mint ID. + * @param affiliate The affiliate's address. + * @return The computed value. + */ + function isAffiliated( + address edition, + uint128 mintId, + address affiliate + ) external view returns (bool); + + /** + * @dev Returns the affiliate Merkle root. + * @param edition The edition's address. + * @param mintId The mint ID. + * @return The latest value. + */ + function affiliateMerkleRoot(address edition, uint128 mintId) external view returns (bytes32); + + /** + * @dev Returns the total price and fees for `quantity` tokens for (`edition`, `mintId`). + * @param edition The edition's address. + * @param mintId The mint ID. + * @param quantity The number of tokens to mint. + * @return total The total buy price, inclusive of any additional platform flat fees. + * @return subTotal The total buy price, exclusive of any additional platform flat fees. + * @return platformFlatFeeTotal The total platform flat fees, which is added onto `subTotal` to give `total`. + * @return platformFee The total platform fees. + * @return affiliateFee The affiliate fees. + */ + function totalPriceAndFees( + address edition, + uint128 mintId, + uint32 quantity + ) + external + view + returns ( + uint256 total, + uint256 subTotal, + uint256 platformFlatFeeTotal, + uint256 platformFee, + uint256 affiliateFee + ); + + /** + * @dev Returns the platform fee basis points. + * @return The configured value. + */ + function platformFeeBPS() external returns (uint16); + + /** + * @dev Returns the platform flat fee per item. + * @return The configured value. + */ + function platformFlatFee() external returns (uint96); + + /** + * @dev Returns the platform flat fee per transaction. + * @return The configured value. + */ + function platformPerTxFlatFee() external returns (uint96); + + /** + * @dev Returns the platform fee address. + * @return The configured value. + */ + function platformFeeAddress() external returns (address); + + /** + * @dev The next mint ID. + * A mint ID is assigned sequentially starting from (0, 1, 2, ...), + * and is shared amongst all editions connected to the minter contract. + * @return The latest value. + */ + function nextMintId() external view returns (uint128); + + /** + * @dev The interface ID of the minter. + * @return The constant value. + */ + function moduleInterfaceId() external view returns (bytes4); +} diff --git a/contracts/core/utils/MintRandomnessLib.sol b/contracts/core/utils/MintRandomnessLib.sol index 2c000c30..3d10ba0f 100644 --- a/contracts/core/utils/MintRandomnessLib.sol +++ b/contracts/core/utils/MintRandomnessLib.sol @@ -52,10 +52,11 @@ library MintRandomnessLib { // Pick any of the last 256 blocks pseudorandomly for the blockhash. mstore(0x00, blockhash(sub(number(), add(1, and(0xff, randomness))))) // After the merge, if [EIP-4399](https://eips.ethereum.org/EIPS/eip-4399) - // is implemented, the `difficulty()` will be determined by the beacon chain. + // is implemented, the `difficulty()` will be determined by the beacon chain, + // and renamed to `prevrandao()`. // We also need to xor with the `totalMinted` to prevent the randomness // from being stucked. - mstore(0x20, xor(xor(randomness, difficulty()), totalMinted)) + mstore(0x20, xor(xor(randomness, prevrandao()), totalMinted)) let r := keccak256(0x00, 0x40) diff --git a/contracts/modules/BaseMinterV2_1.sol b/contracts/modules/BaseMinterV2_1.sol new file mode 100644 index 00000000..de765821 --- /dev/null +++ b/contracts/modules/BaseMinterV2_1.sol @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; +import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; + +/** + * @title Minter Base + * @dev The `BaseMinterV2_1` class maintains a central storage record of edition mint instances. + */ +abstract contract BaseMinterV2_1 is IMinterModuleV2_1, Ownable { + // ============================================================= + // CONSTANTS + // ============================================================= + + /** + * @dev This is the denominator, in basis points (BPS), for any of the fees. + */ + uint16 public constant BPS_DENOMINATOR = 10_000; + + /** + * @dev The maximum basis points (BPS) limit allowed for the affiliate fees. + */ + uint16 public constant MAX_AFFILIATE_FEE_BPS = 1000; + + /** + * @dev The maximum basis points (BPS) limit allowed for the platform fees. + */ + uint16 public constant MAX_PLATFORM_FEE_BPS = 1000; + + /** + * @dev The maximum platform flat fee per transaction NFT. + */ + uint96 public constant MAX_PLATFORM_PER_TX_FLAT_FEE = 0.1 ether; + + /** + * @dev The maximum platform flat fee per NFT. + */ + uint96 public constant MAX_PLATFORM_FLAT_FEE = 0.1 ether; + + /** + * @dev The interface id for IMinterModuleV2. + */ + bytes4 internal constant _INTERFACE_ID_MINTER_MODULE_V2 = 0xf8ccd08e; + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev The platform fee address. + */ + address public platformFeeAddress; + + /** + * @dev The next mint ID. Shared amongst all editions connected. + */ + uint96 private _nextMintId; + + /** + * @dev How much platform fees have been accrued. + */ + uint128 public platformFeesAccrued; + + /** + * @dev The amount of platform flat fees per token. + */ + uint96 public platformFlatFee; + + /** + * @dev The platform fee in basis points. + */ + uint16 public platformFeeBPS; + + /** + * @dev The amount of platform flat fees per transaction. + */ + uint96 public platformPerTxFlatFee; + + /** + * @dev Maps an edition and the mint ID to a mint instance. + */ + mapping(address => mapping(uint256 => BaseData)) private _baseData; + + /** + * @dev Maps an address to how much affiliate fees have they accrued. + */ + mapping(address => uint128) public affiliateFeesAccrued; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor() payable { + _initializeOwner(msg.sender); + } + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + // Per edition mint parameter setters: + // ----------------------------------- + // These functions can only be called by the owner or admin of the edition. + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setEditionMintPaused( + address edition, + uint128 mintId, + bool paused + ) public virtual onlyEditionOwnerOrAdmin(edition) { + _getBaseData(edition, mintId).mintPaused = paused; + emit MintPausedSet(edition, mintId, paused); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setTimeRange( + address edition, + uint128 mintId, + uint32 startTime, + uint32 endTime + ) public virtual onlyEditionOwnerOrAdmin(edition) { + if (startTime >= endTime) revert InvalidTimeRange(); + BaseData storage baseData = _getBaseData(edition, mintId); + baseData.startTime = startTime; + baseData.endTime = endTime; + emit TimeRangeSet(edition, mintId, startTime, endTime); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setAffiliateFee( + address edition, + uint128 mintId, + uint16 bps + ) public virtual override onlyEditionOwnerOrAdmin(edition) { + if (bps > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS(); + _getBaseData(edition, mintId).affiliateFeeBPS = bps; + emit AffiliateFeeSet(edition, mintId, bps); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setAffiliateMerkleRoot( + address edition, + uint128 mintId, + bytes32 root + ) public virtual override onlyEditionOwnerOrAdmin(edition) { + _getBaseData(edition, mintId).affiliateMerkleRoot = root; + emit AffiliateMerkleRootSet(edition, mintId, root); + } + + // Withdrawal functions: + // --------------------- + // These functions can be called by anyone. + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function withdrawForAffiliate(address affiliate) public override { + uint128 accrued = affiliateFeesAccrued[affiliate]; + if (accrued != 0) { + affiliateFeesAccrued[affiliate] = 0; + SafeTransferLib.forceSafeTransferETH(affiliate, accrued); + emit AffiliateFeesWithdrawn(affiliate, accrued); + } + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function withdrawForPlatform() public override { + address to = platformFeeAddress; + if (to == address(0)) revert PlatformFeeAddressIsZero(); + uint128 accrued = platformFeesAccrued; + if (accrued != 0) { + platformFeesAccrued = 0; + SafeTransferLib.forceSafeTransferETH(to, accrued); + emit PlatformFeesWithdrawn(accrued); + } + } + + // Only owner setters: + // ------------------- + // These functions can only be called by the owner of the minter contract. + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setPlatformFee(uint16 bps) public onlyOwner { + if (bps > MAX_PLATFORM_FEE_BPS) revert InvalidPlatformFeeBPS(); + platformFeeBPS = bps; + emit PlatformFeeSet(bps); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setPlatformFlatFee(uint96 flatFee) public onlyOwner { + if (flatFee > MAX_PLATFORM_FLAT_FEE) revert InvalidPlatformFlatFee(); + platformFlatFee = flatFee; + emit PlatformFlatFeeSet(flatFee); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setPlatformPerTxFlatFee(uint96 perTxFlatFee) public onlyOwner { + if (perTxFlatFee > MAX_PLATFORM_PER_TX_FLAT_FEE) revert InvalidPlatformPerTxFlatFee(); + platformPerTxFlatFee = perTxFlatFee; + emit PlatformPerTxFlatFeeSet(perTxFlatFee); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function setPlatformFeeAddress(address addr) public onlyOwner { + if (addr == address(0)) revert PlatformFeeAddressIsZero(); + platformFeeAddress = addr; + emit PlatformFeeAddressSet(addr); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function nextMintId() external view returns (uint128) { + return _nextMintId; + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function isAffiliatedWithProof( + address edition, + uint128 mintId, + address affiliate, + bytes32[] calldata affiliateProof + ) public view virtual override returns (bool) { + bytes32 root = _getBaseData(edition, mintId).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 + affiliate != address(0) && + MerkleProofLib.verifyCalldata(affiliateProof, root, _keccak256EncodePacked(affiliate)); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function isAffiliated( + address edition, + uint128 mintId, + address affiliate + ) public view virtual override returns (bool) { + return isAffiliatedWithProof(edition, mintId, affiliate, MerkleProofLib.emptyProof()); + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function affiliateMerkleRoot(address edition, uint128 mintId) external view returns (bytes32) { + return _getBaseData(edition, mintId).affiliateMerkleRoot; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == _INTERFACE_ID_MINTER_MODULE_V2 || + interfaceId == type(IMinterModuleV2_1).interfaceId || + interfaceId == this.supportsInterface.selector; + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function totalPriceAndFees( + address edition, + uint128 mintId, + uint32 quantity + ) + public + view + virtual + override + returns ( + uint256 total, + uint256 subTotal, + uint256 platformFlatFeeTotal, + uint256 platformFee, + uint256 affiliateFee + ) + { + BaseData storage baseData = _getBaseData(edition, mintId); + + unchecked { + subTotal = uint256(quantity) * uint256(baseData.price); // Before additional fees. + + platformFlatFeeTotal = uint256(quantity) * uint256(platformFlatFee) + uint256(platformPerTxFlatFee); + + total = subTotal + platformFlatFeeTotal; + + platformFee = (subTotal * uint256(platformFeeBPS)) / uint256(BPS_DENOMINATOR) + platformFlatFeeTotal; + + affiliateFee = (subTotal * uint256(baseData.affiliateFeeBPS)) / uint256(BPS_DENOMINATOR); + } + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + /** + * @dev Requires that the caller is the owner or admin of `edition`. + * @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.sender(); + if (sender != OwnableRoles(edition).owner()) + if (!OwnableRoles(edition).hasAnyRole(sender, ISoundEditionV1_2(edition).ADMIN_ROLE())) + revert Unauthorized(); + } + + /** + * @dev If overriden to return true, the amount of ETH paid must be exact. + * @return The constant value. + */ + function _useExactPayment() internal view virtual returns (bool) { + return true; + } + + /** + * @dev Creates an edition mint instance. + * @param edition The edition address. + * @param startTime The start time of the mint. + * @param endTime The end time of the mint. + * @param affiliateFeeBPS The affiliate fee in basis points. + * @return mintId The ID for the mint instance. + * Calling conditions: + * - Must be owner or admin of the edition. + */ + function _createEditionMint( + address edition, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS + ) internal onlyEditionOwnerOrAdmin(edition) returns (uint128 mintId) { + if (startTime >= endTime) revert InvalidTimeRange(); + if (affiliateFeeBPS > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS(); + + mintId = _nextMintId; + + BaseData storage data = _getBaseDataUnchecked(edition, mintId); + data.startTime = startTime; + data.endTime = endTime; + data.affiliateFeeBPS = affiliateFeeBPS; + data.created = true; + + unchecked { + _nextMintId = SafeCastLib.toUint96(mintId + 1); + } + + emit MintConfigCreated(edition, msg.sender, mintId, startTime, endTime, affiliateFeeBPS); + } + + /** + * For avoiding stack too deep. + */ + struct _MintTemps { + bool affiliated; + uint256 affiliateFee; + uint256 remainingPayment; + uint256 platformFee; + uint256 requiredEtherValue; + } + + /** + * @dev Mints `quantity` of `edition` to `to` with a required payment of `requiredEtherValue`. + * Note: this function should be called at the end of a function due to it refunding any + * excess ether paid, to adhere to the checks-effects-interactions pattern. + * Otherwise, a reentrancy guard must be used. + * @param edition The edition address. + * @param mintId The ID for the mint instance. + * @param to The address to mint to. + * @param quantity The quantity of tokens to mint. + * @param affiliate The affiliate (referral) address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @param attributionId The attribution ID. + */ + function _mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) internal { + BaseData storage baseData = _getBaseData(edition, mintId); + _MintTemps memory t; + + /* --------------------- GENERAL CHECKS --------------------- */ + { + uint32 startTime = baseData.startTime; + uint32 endTime = baseData.endTime; + if (block.timestamp < startTime) revert MintNotOpen(block.timestamp, startTime, endTime); + if (block.timestamp > endTime) revert MintNotOpen(block.timestamp, startTime, endTime); + if (baseData.mintPaused) revert MintPaused(); + } + + /* ----------- AFFILIATE AND PLATFORM FEES LOGIC ------------ */ + + unchecked { + // `requiredEtherValue = platformFee + affiliateFee + artistFee`. + (t.requiredEtherValue, , , t.platformFee, t.affiliateFee) = totalPriceAndFees(edition, mintId, quantity); + + // Reverts if the payment is not exact, or not enough. + if (_useExactPayment()) { + if (msg.value != t.requiredEtherValue) revert WrongPayment(msg.value, t.requiredEtherValue); + } else { + if (msg.value < t.requiredEtherValue) revert Underpaid(msg.value, t.requiredEtherValue); + } + + // Increment the platform fees accrued. + platformFeesAccrued = SafeCastLib.toUint128(uint256(platformFeesAccrued) + t.platformFee); + // Deduct the platform BPS fee, and the platform flat fees. + // Won't underflow as `platformFee <= requiredEtherValue`; + t.remainingPayment = t.requiredEtherValue - t.platformFee; + } + + // Check if the mint is an affiliated mint. + t.affiliated = isAffiliatedWithProof(edition, mintId, affiliate, affiliateProof); + unchecked { + if (t.affiliated) { + // Deduct the affiliate fee from the remaining payment. + // Won't underflow as `affiliateFee <= remainingPayment`. + t.remainingPayment -= t.affiliateFee; + // Increment the affiliate fees accrued. + affiliateFeesAccrued[affiliate] = SafeCastLib.toUint128( + uint256(affiliateFeesAccrued[affiliate]) + t.affiliateFee + ); + } else { + t.affiliateFee = 0; + // If the affiliate is not the zero address despite not being + // affiliated, it might be due to an invalid affiliate proof. + // Revert to prevent unintended skipping of affiliate payment. + if (affiliate != address(0)) { + revert InvalidAffiliate(); + } + } + } + + /* ------------------------- MINT --------------------------- */ + + // Emit the event. + emit Minted( + edition, + mintId, + to, + // Need to put this call here to avoid stack-too-deep error (it returns `fromTokenId`). + uint32(ISoundEditionV1_2(edition).mint{ value: t.remainingPayment }(to, quantity)), + quantity, + uint128(t.requiredEtherValue), + uint128(t.platformFee), + uint128(t.affiliateFee), + affiliate, + t.affiliated, + attributionId + ); + + /* ------------------------- REFUND ------------------------- */ + + unchecked { + if (!_useExactPayment()) { + // Note: We do this at the end to avoid creating a reentrancy vector. + // Refund the user any ETH they spent over the current total price of the NFTs. + if (msg.value > t.requiredEtherValue) { + SafeTransferLib.forceSafeTransferETH(msg.sender, msg.value - t.requiredEtherValue); + } + } + } + } + + /** + * @dev Increments `totalMinted` with `quantity`, reverting if `totalMinted + quantity > maxMintable`. + * @param totalMinted The current total number of minted tokens. + * @param maxMintable The maximum number of mintable tokens. + * @return `totalMinted` + `quantity`. + */ + function _incrementTotalMinted( + uint32 totalMinted, + uint32 quantity, + uint32 maxMintable + ) internal pure returns (uint32) { + unchecked { + // Won't overflow as both are 32 bits. + uint256 sum = uint256(totalMinted) + uint256(quantity); + if (sum > maxMintable) { + // Note that the `maxMintable` may vary and drop over time + // and cause `totalMinted` to be greater than `maxMintable`. + // The `zeroFloorSub` is equivalent to `max(0, x - y)`. + uint32 available = uint32(FixedPointMathLib.zeroFloorSub(maxMintable, totalMinted)); + revert ExceedsAvailableSupply(available); + } + return uint32(sum); + } + } + + /** + * @dev Returns the storage pointer to the BaseData for (`edition`, `mintId`). + * @param edition The edition address. + * @param mintId The mint ID. + * @return data Storage pointer to a BaseData. + */ + function _getBaseDataUnchecked(address edition, uint128 mintId) internal view returns (BaseData storage data) { + data = _baseData[edition][mintId]; + } + + /** + * @dev Returns the storage pointer to the BaseData for (`edition`, `mintId`). + * Reverts if the mint does not exist. + * @param edition The edition address. + * @param mintId The mint ID. + * @return data Storage pointer to a BaseData. + */ + function _getBaseData(address edition, uint128 mintId) internal view returns (BaseData storage data) { + data = _getBaseDataUnchecked(edition, mintId); + if (!data.created) revert MintDoesNotExist(); + } + + /** + * @dev Casts the storage pointer to the BaseData to a bytes32. + * @param data Storage pointer to a BaseData. + * @return result The casted value of the slot. + */ + function _baseDataSlot(BaseData storage data) internal pure returns (bytes32 result) { + assembly { + result := data.slot + } + } + + /** + * @dev Equivalent to `keccak256(abi.encodePacked(addr))`. + * @param addr The address to hash. + * @return result The hash of the address. + */ + function _keccak256EncodePacked(address addr) internal pure returns (bytes32 result) { + assembly { + mstore(0x00, addr) + result := keccak256(0x0c, 0x14) + } + } +} diff --git a/contracts/modules/EditionMaxMinterV2_1.sol b/contracts/modules/EditionMaxMinterV2_1.sol new file mode 100644 index 00000000..2014d8dd --- /dev/null +++ b/contracts/modules/EditionMaxMinterV2_1.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { IEditionMaxMinterV2_1, MintInfo } from "./interfaces/IEditionMaxMinterV2_1.sol"; +import { BaseMinterV2_1 } from "./BaseMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { ISoundEditionV1, EditionInfo } from "@core/interfaces/ISoundEditionV1.sol"; + +/* + * @title EditionMaxMinterV2_1 + * @notice Module for unpermissioned mints of Sound editions. + * @author Sound.xyz + */ +contract EditionMaxMinterV2_1 is IEditionMaxMinterV2_1, BaseMinterV2_1 { + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function createEditionMint( + address edition, + uint96 price, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintablePerAccount + ) public returns (uint128 mintId) { + if (maxMintablePerAccount == 0) revert MaxMintablePerAccountIsZero(); + + mintId = _createEditionMint(edition, startTime, endTime, affiliateFeeBPS); + + BaseData storage data = _getBaseDataUnchecked(edition, mintId); + data.price = price; + data.maxMintablePerAccount = maxMintablePerAccount; + + // prettier-ignore + emit EditionMaxMintCreated( + edition, + mintId, + price, + startTime, + endTime, + affiliateFeeBPS, + maxMintablePerAccount + ); + } + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) public payable { + BaseData storage baseData = _getBaseData(edition, mintId); + + unchecked { + // Check the additional `requestedQuantity` does not exceed the maximum mintable per account. + uint256 numberMinted = ISoundEditionV1(edition).numberMinted(to); + // Won't overflow. The total number of tokens minted in `edition` won't exceed `type(uint32).max`, + // and `quantity` has 32 bits. + if (numberMinted + quantity > baseData.maxMintablePerAccount) revert ExceedsMaxPerAccount(); + } + + _mintTo(edition, mintId, to, quantity, affiliate, affiliateProof, attributionId); + } + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + address affiliate + ) public payable { + mintTo(edition, mintId, msg.sender, quantity, affiliate, MerkleProofLib.emptyProof(), 0); + } + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) public onlyEditionOwnerOrAdmin(edition) { + _getBaseData(edition, mintId).price = price; + emit PriceSet(edition, mintId, price); + } + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function setMaxMintablePerAccount( + address edition, + uint128 mintId, + uint32 maxMintablePerAccount + ) public onlyEditionOwnerOrAdmin(edition) { + if (maxMintablePerAccount == 0) revert MaxMintablePerAccountIsZero(); + _getBaseData(edition, mintId).maxMintablePerAccount = maxMintablePerAccount; + emit MaxMintablePerAccountSet(edition, mintId, maxMintablePerAccount); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory info) { + BaseData memory baseData = _getBaseData(edition, mintId); + + EditionInfo memory editionInfo = ISoundEditionV1(edition).editionInfo(); + + info.startTime = baseData.startTime; + info.endTime = baseData.endTime; + info.affiliateFeeBPS = baseData.affiliateFeeBPS; + info.mintPaused = baseData.mintPaused; + info.price = baseData.price; + info.maxMintablePerAccount = baseData.maxMintablePerAccount; + + info.maxMintableLower = editionInfo.editionMaxMintableLower; + info.maxMintableUpper = editionInfo.editionMaxMintableUpper; + info.totalMinted = uint32(editionInfo.totalMinted); + info.cutoffTime = editionInfo.editionCutoffTime; + + info.affiliateMerkleRoot = baseData.affiliateMerkleRoot; + info.platformFeeBPS = platformFeeBPS; + info.platformFlatFee = platformFlatFee; + info.platformPerTxFlatFee = platformPerTxFlatFee; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view override(IERC165, BaseMinterV2_1) returns (bool) { + return BaseMinterV2_1.supportsInterface(interfaceId) || interfaceId == type(IEditionMaxMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function moduleInterfaceId() public pure returns (bytes4) { + return type(IEditionMaxMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IEditionMaxMinterV2_1 + */ + function isV2_1() external pure override returns (bool) { + return true; + } +} diff --git a/contracts/modules/FixedPriceSignatureMinterV2_1.sol b/contracts/modules/FixedPriceSignatureMinterV2_1.sol new file mode 100644 index 00000000..9221b6a9 --- /dev/null +++ b/contracts/modules/FixedPriceSignatureMinterV2_1.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { ECDSA } from "solady/utils/ECDSA.sol"; +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { LibBitmap } from "solady/utils/LibBitmap.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { BaseMinterV2_1 } from "@modules/BaseMinterV2_1.sol"; +import { IFixedPriceSignatureMinterV2_1, EditionMintData, MintInfo } from "./interfaces/IFixedPriceSignatureMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; + +/** + * @title IFixedPriceSignatureMinterV2_1 + * @dev Module for fixed-price, signature-authorized mints of Sound editions. + * @author Sound.xyz + */ +contract FixedPriceSignatureMinterV2_1 is IFixedPriceSignatureMinterV2_1, BaseMinterV2_1 { + using ECDSA for bytes32; + using LibBitmap for *; + + // ============================================================= + // CONSTANTS + // ============================================================= + + /** + * @dev EIP-712 Typed structured data hash (used for checking signature validity). + * https://eips.ethereum.org/EIPS/eip-712 + */ + bytes32 public constant MINT_TYPEHASH = + keccak256( + "EditionInfo(address buyer,uint128 mintId,uint32 claimTicket,uint32 signedQuantity,address affiliate)" + ); + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev Edition mint data + * `_baseDataSlot(_getBaseData(edition, mintId))` => value. + */ + mapping(bytes32 => EditionMintData) internal _editionMintData; + + /** + * @dev A mapping of bitmaps where each bit represents whether the ticket has been claimed. + * `_baseDataSlot(_getBaseData(edition, mintId))` => `index` => bit array + */ + mapping(bytes32 => LibBitmap.Bitmap) internal _claimsBitmaps; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function createEditionMint( + address edition, + uint96 price, + address signer, + uint32 maxMintable, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS + ) public returns (uint128 mintId) { + if (signer == address(0)) revert SignerIsZeroAddress(); + mintId = _createEditionMint(edition, startTime, endTime, affiliateFeeBPS); + + BaseData storage baseData = _getBaseDataUnchecked(edition, mintId); + baseData.price = price; + + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + data.signer = signer; + data.maxMintable = maxMintable; + // prettier-ignore + emit FixedPriceSignatureMintCreated( + edition, + mintId, + price, + signer, + maxMintable, + startTime, + endTime, + affiliateFeeBPS + ); + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + uint32 signedQuantity, + address affiliate, + bytes32[] calldata affiliateProof, + bytes calldata signature, + uint32 claimTicket, + uint256 attributionId + ) public payable { + if (quantity > signedQuantity) revert ExceedsSignedQuantity(); + + // Compute the digest here to avoid stack too deep. + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(MINT_TYPEHASH, to, mintId, claimTicket, signedQuantity, msg.sender)) + ) + ); + + bytes32 baseDataSlot = _baseDataSlot(_getBaseData(edition, mintId)); + + EditionMintData storage data = _editionMintData[baseDataSlot]; + + // Just in case. + // For an uninitialized mint, `data.maxMintable` will be zero, which will not allow any mints. + // But we include this check, just in case the condition is removed in the future. + if (data.signer == address(0)) revert SignerIsZeroAddress(); + + data.totalMinted = _incrementTotalMinted(data.totalMinted, quantity, data.maxMintable); + + // Validate the signature. + if (digest.recoverCalldata(signature) != data.signer) revert InvalidSignature(); + + // Toggle the bit for the `claimTicket`. + // If the toggled value is false, it means that it has already been used. + if (!_claimsBitmaps[baseDataSlot].toggle(claimTicket)) revert SignatureAlreadyUsed(); + + _mintTo(edition, mintId, to, quantity, affiliate, affiliateProof, attributionId); + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + uint32 signedQuantity, + address affiliate, + bytes calldata signature, + uint32 claimTicket + ) public payable { + mintTo( + edition, + mintId, + msg.sender, + quantity, + signedQuantity, + affiliate, + MerkleProofLib.emptyProof(), + signature, + claimTicket, + 0 + ); + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function setMaxMintable( + address edition, + uint128 mintId, + uint32 maxMintable + ) public onlyEditionOwnerOrAdmin(edition) { + _editionMintData[_baseDataSlot(_getBaseData(edition, mintId))].maxMintable = maxMintable; + emit MaxMintableSet(edition, mintId, maxMintable); + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) public onlyEditionOwnerOrAdmin(edition) { + _getBaseData(edition, mintId).price = price; + emit PriceSet(edition, mintId, price); + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function setSigner( + address edition, + uint128 mintId, + address signer + ) public onlyEditionOwnerOrAdmin(edition) { + if (signer == address(0)) revert SignerIsZeroAddress(); + _editionMintData[_baseDataSlot(_getBaseData(edition, mintId))].signer = signer; + emit SignerSet(edition, mintId, signer); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function mintInfo(address edition, uint128 mintId) external view override returns (MintInfo memory info) { + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage mintData = _editionMintData[_baseDataSlot(baseData)]; + + info.startTime = baseData.startTime; + info.endTime = baseData.endTime; + info.affiliateFeeBPS = baseData.affiliateFeeBPS; + info.mintPaused = baseData.mintPaused; + info.price = baseData.price; + info.maxMintable = mintData.maxMintable; + info.maxMintablePerAccount = type(uint32).max; + info.totalMinted = mintData.totalMinted; + info.signer = mintData.signer; + + info.affiliateMerkleRoot = baseData.affiliateMerkleRoot; + info.platformFeeBPS = platformFeeBPS; + info.platformFlatFee = platformFlatFee; + info.platformPerTxFlatFee = platformPerTxFlatFee; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view override(IERC165, BaseMinterV2_1) returns (bool) { + return + BaseMinterV2_1.supportsInterface(interfaceId) || + interfaceId == type(IFixedPriceSignatureMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function moduleInterfaceId() public pure returns (bytes4) { + return type(IFixedPriceSignatureMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function checkClaimTickets( + address edition, + uint128 mintId, + uint32[] calldata claimTickets + ) external view returns (bool[] memory claimed) { + LibBitmap.Bitmap storage bitmap = _claimsBitmaps[_baseDataSlot(_getBaseData(edition, mintId))]; + claimed = new bool[](claimTickets.length); + // Will not overflow due to max block gas limit bounding the size of `claimTickets`. + unchecked { + for (uint256 i = 0; i < claimTickets.length; i++) { + claimed[i] = bitmap.get(claimTickets[i]); + } + } + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function DOMAIN_SEPARATOR() public view returns (bytes32 separator) { + separator = keccak256( + abi.encode( + keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"), + block.chainid, + address(this) + ) + ); + } + + /** + * @inheritdoc IFixedPriceSignatureMinterV2_1 + */ + function isV2_1() external pure override returns (bool) { + return true; + } +} diff --git a/contracts/modules/MerkleDropMinterV2_1.sol b/contracts/modules/MerkleDropMinterV2_1.sol new file mode 100644 index 00000000..b5820df4 --- /dev/null +++ b/contracts/modules/MerkleDropMinterV2_1.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { BaseMinterV2_1 } from "@modules/BaseMinterV2_1.sol"; +import { DelegateCashLib } from "@modules/utils/DelegateCashLib.sol"; +import { IMerkleDropMinterV2_1, EditionMintData, MintInfo } from "./interfaces/IMerkleDropMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; + +/** + * @title MerkleDropMinterV2_1 + * @dev Module for minting Sound editions using a merkle tree of approved accounts. + * @author Sound.xyz + */ +contract MerkleDropMinterV2_1 is IMerkleDropMinterV2_1, BaseMinterV2_1 { + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev Edition mint data. + * `_baseDataSlot(_getBaseData(edition, mintId))` => value. + */ + mapping(bytes32 => EditionMintData) internal _editionMintData; + + /** + * @dev The number of mints for each account. + * `_baseDataSlot(_getBaseData(edition, mintId))` => `address` => value. + * We will simply store a uint256 for every account, to keep the Merkle tree + * simple, so that it is compatible with 3rd party allowlist services like Lanyard. + */ + mapping(bytes32 => mapping(address => uint256)) internal _mintCounts; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function createEditionMint( + address edition, + bytes32 merkleRootHash, + uint96 price, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintable, + uint32 maxMintablePerAccount + ) public returns (uint128 mintId) { + if (merkleRootHash == bytes32(0)) revert MerkleRootHashIsEmpty(); + if (maxMintablePerAccount == 0) revert MaxMintablePerAccountIsZero(); + + mintId = _createEditionMint(edition, startTime, endTime, affiliateFeeBPS); + + BaseData storage baseData = _getBaseDataUnchecked(edition, mintId); + baseData.price = price; + baseData.maxMintablePerAccount = maxMintablePerAccount; + + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + data.merkleRootHash = merkleRootHash; + data.maxMintable = maxMintable; + + // prettier-ignore + emit MerkleDropMintCreated( + edition, + mintId, + merkleRootHash, + price, + startTime, + endTime, + affiliateFeeBPS, + maxMintable, + maxMintablePerAccount + ); + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address allowlisted, + bytes32[] calldata proof, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) public payable { + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + + // Increase `totalMinted` by `quantity`. + // Require that the increased value does not exceed `maxMintable`. + data.totalMinted = _incrementTotalMinted(data.totalMinted, quantity, data.maxMintable); + + // Verify that `allowlisted` is in the Merkle tree with the `proof`. + // We also 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) || + !MerkleProofLib.verifyCalldata(proof, data.merkleRootHash, _keccak256EncodePacked(allowlisted)) + ) revert InvalidMerkleProof(); + + // To mint, either `msg.sender` or `to` must be equal to `allowlisted`, + // or `msg.sender` must be a delegate of `allowlisted`. + if (msg.sender != allowlisted && to != allowlisted) + if (!DelegateCashLib.checkDelegateForAll(msg.sender, allowlisted)) revert CallerNotDelegated(); + + unchecked { + // Check that the additional `quantity` does not exceed the maximum mintable per account. + // Won't overflow, as `maxMintablePerAccount` and `quantity` are 32 bits. + if ((_mintCounts[_baseDataSlot(baseData)][allowlisted] += quantity) > baseData.maxMintablePerAccount) + revert ExceedsMaxPerAccount(); + } + + _mintTo(edition, mintId, to, quantity, affiliate, affiliateProof, attributionId); + + emit DropClaimed(allowlisted, quantity); + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + bytes32[] calldata proof, + address affiliate + ) public payable { + mintTo(edition, mintId, msg.sender, quantity, msg.sender, proof, affiliate, MerkleProofLib.emptyProof(), 0); + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) public onlyEditionOwnerOrAdmin(edition) { + _getBaseData(edition, mintId).price = price; + emit PriceSet(edition, mintId, price); + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function setMaxMintablePerAccount( + address edition, + uint128 mintId, + uint32 maxMintablePerAccount + ) public onlyEditionOwnerOrAdmin(edition) { + if (maxMintablePerAccount == 0) revert MaxMintablePerAccountIsZero(); + _getBaseData(edition, mintId).maxMintablePerAccount = maxMintablePerAccount; + emit MaxMintablePerAccountSet(edition, mintId, maxMintablePerAccount); + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function setMaxMintable( + address edition, + uint128 mintId, + uint32 maxMintable + ) public onlyEditionOwnerOrAdmin(edition) { + _editionMintData[_baseDataSlot(_getBaseData(edition, mintId))].maxMintable = maxMintable; + emit MaxMintableSet(edition, mintId, maxMintable); + } + + /* + * @inheritdoc IMerkleDropMinterV2_1 + */ + function setMerkleRootHash( + address edition, + uint128 mintId, + bytes32 merkleRootHash + ) public onlyEditionOwnerOrAdmin(edition) { + if (merkleRootHash == bytes32(0)) revert MerkleRootHashIsEmpty(); + _editionMintData[_baseDataSlot(_getBaseData(edition, mintId))].merkleRootHash = merkleRootHash; + emit MerkleRootHashSet(edition, mintId, merkleRootHash); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function mintCount( + address edition, + uint128 mintId, + address to + ) public view virtual returns (uint256) { + return _mintCounts[_baseDataSlot(_getBaseData(edition, mintId))][to]; + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory info) { + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage mintData = _editionMintData[_baseDataSlot(baseData)]; + + info.startTime = baseData.startTime; + info.endTime = baseData.endTime; + info.affiliateFeeBPS = baseData.affiliateFeeBPS; + info.mintPaused = baseData.mintPaused; + info.price = baseData.price; + info.maxMintable = mintData.maxMintable; + info.maxMintablePerAccount = baseData.maxMintablePerAccount; + info.totalMinted = mintData.totalMinted; + info.merkleRootHash = mintData.merkleRootHash; + + info.affiliateMerkleRoot = baseData.affiliateMerkleRoot; + info.platformFeeBPS = platformFeeBPS; + info.platformFlatFee = platformFlatFee; + info.platformPerTxFlatFee = platformPerTxFlatFee; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view override(IERC165, BaseMinterV2_1) returns (bool) { + return BaseMinterV2_1.supportsInterface(interfaceId) || interfaceId == type(IMerkleDropMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function moduleInterfaceId() public pure returns (bytes4) { + return type(IMerkleDropMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IMerkleDropMinterV2_1 + */ + function isV2_1() external pure override returns (bool) { + return true; + } +} diff --git a/contracts/modules/MinterAdapter.sol b/contracts/modules/MinterAdapter.sol index 873d203b..d257ad4c 100644 --- a/contracts/modules/MinterAdapter.sol +++ b/contracts/modules/MinterAdapter.sol @@ -10,7 +10,7 @@ import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; /** * @title Minter Adapter * @dev A minter adapter for minting to user specified addresses on - * old EditionMaxMinterV2s and RangeEditionMinterV2s, + * old EditionMaxMinterV2_1s and RangeEditionMinterV2_1s, * which do not have a `mintTo` function. */ contract MinterAdapter is IMinterAdapter { diff --git a/contracts/modules/RangeEditionMinterV2_1.sol b/contracts/modules/RangeEditionMinterV2_1.sol new file mode 100644 index 00000000..ffd7f9e9 --- /dev/null +++ b/contracts/modules/RangeEditionMinterV2_1.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { IRangeEditionMinterV2_1, EditionMintData, MintInfo } from "./interfaces/IRangeEditionMinterV2_1.sol"; +import { BaseMinterV2_1 } from "./BaseMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; + +/* + * @title RangeEditionMinterV2_1 + * @notice Module for range edition mints of Sound editions. + * @author Sound.xyz + */ +contract RangeEditionMinterV2_1 is IRangeEditionMinterV2_1, BaseMinterV2_1 { + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev Edition mint data. + * `_baseDataSlot(_getBaseData(edition, mintId))` => value. + */ + mapping(bytes32 => EditionMintData) internal _editionMintData; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function createEditionMint( + address edition, + uint96 price, + uint32 startTime, + uint32 cutoffTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintableLower, + uint32 maxMintableUpper, + uint32 maxMintablePerAccount + ) public returns (uint128 mintId) { + _requireValidCombinedTimeRange(startTime, cutoffTime, endTime); + if (maxMintableLower > maxMintableUpper) revert InvalidMaxMintableRange(); + if (maxMintablePerAccount == 0) revert MaxMintablePerAccountIsZero(); + + mintId = _createEditionMint(edition, startTime, endTime, affiliateFeeBPS); + + BaseData storage baseData = _getBaseDataUnchecked(edition, mintId); + baseData.price = price; + baseData.maxMintablePerAccount = maxMintablePerAccount; + + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + data.cutoffTime = cutoffTime; + data.maxMintableLower = maxMintableLower; + data.maxMintableUpper = maxMintableUpper; + + // prettier-ignore + emit RangeEditionMintCreated( + edition, + mintId, + price, + startTime, + cutoffTime, + endTime, + affiliateFeeBPS, + maxMintableLower, + maxMintableUpper, + maxMintablePerAccount + ); + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) public payable { + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + + uint32 _maxMintable = _getMaxMintable(data); + + // Increase `totalMinted` by `quantity`. + // Require that the increased value does not exceed `maxMintable`. + data.totalMinted = _incrementTotalMinted(data.totalMinted, quantity, _maxMintable); + + unchecked { + // Check the additional `requestedQuantity` does not exceed the maximum mintable per account. + uint256 numberMinted = ISoundEditionV1(edition).numberMinted(to); + // Won't overflow. The total number of tokens minted in `edition` won't exceed `type(uint32).max`, + // and `quantity` has 32 bits. + if (numberMinted + quantity > baseData.maxMintablePerAccount) revert ExceedsMaxPerAccount(); + } + + _mintTo(edition, mintId, to, quantity, affiliate, affiliateProof, attributionId); + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + address affiliate + ) public payable { + mintTo(edition, mintId, msg.sender, quantity, affiliate, MerkleProofLib.emptyProof(), 0); + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function setTimeRange( + address edition, + uint128 mintId, + uint32 startTime, + uint32 cutoffTime, + uint32 endTime + ) public onlyEditionOwnerOrAdmin(edition) { + _requireValidCombinedTimeRange(startTime, cutoffTime, endTime); + + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + + data.cutoffTime = cutoffTime; + baseData.startTime = startTime; + baseData.endTime = endTime; + + emit CutoffTimeSet(edition, mintId, cutoffTime); + emit TimeRangeSet(edition, mintId, startTime, endTime); + } + + /** + * @inheritdoc BaseMinterV2_1 + */ + function setTimeRange( + address edition, + uint128 mintId, + uint32 startTime, + uint32 endTime + ) public override(BaseMinterV2_1, IMinterModuleV2_1) onlyEditionOwnerOrAdmin(edition) { + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage data = _editionMintData[_baseDataSlot(baseData)]; + + _requireValidCombinedTimeRange(startTime, data.cutoffTime, endTime); + + baseData.startTime = startTime; + baseData.endTime = endTime; + + emit TimeRangeSet(edition, mintId, startTime, endTime); + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function setMaxMintableRange( + address edition, + uint128 mintId, + uint32 maxMintableLower, + uint32 maxMintableUpper + ) public onlyEditionOwnerOrAdmin(edition) { + if (maxMintableLower > maxMintableUpper) revert InvalidMaxMintableRange(); + EditionMintData storage data = _editionMintData[_baseDataSlot(_getBaseData(edition, mintId))]; + data.maxMintableLower = maxMintableLower; + data.maxMintableUpper = maxMintableUpper; + + emit MaxMintableRangeSet(edition, mintId, maxMintableLower, maxMintableUpper); + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) public onlyEditionOwnerOrAdmin(edition) { + _getBaseData(edition, mintId).price = price; + emit PriceSet(edition, mintId, price); + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function setMaxMintablePerAccount( + address edition, + uint128 mintId, + uint32 maxMintablePerAccount + ) public onlyEditionOwnerOrAdmin(edition) { + if (maxMintablePerAccount == 0) revert MaxMintablePerAccountIsZero(); + _getBaseData(edition, mintId).maxMintablePerAccount = maxMintablePerAccount; + emit MaxMintablePerAccountSet(edition, mintId, maxMintablePerAccount); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory info) { + BaseData storage baseData = _getBaseData(edition, mintId); + EditionMintData storage mintData = _editionMintData[_baseDataSlot(baseData)]; + + info.startTime = baseData.startTime; + info.endTime = baseData.endTime; + info.affiliateFeeBPS = baseData.affiliateFeeBPS; + info.mintPaused = baseData.mintPaused; + info.price = baseData.price; + info.maxMintableUpper = mintData.maxMintableUpper; + info.maxMintableLower = mintData.maxMintableLower; + info.maxMintablePerAccount = baseData.maxMintablePerAccount; + info.totalMinted = mintData.totalMinted; + info.cutoffTime = mintData.cutoffTime; + + info.affiliateMerkleRoot = baseData.affiliateMerkleRoot; + info.platformFeeBPS = platformFeeBPS; + info.platformFlatFee = platformFlatFee; + info.platformPerTxFlatFee = platformPerTxFlatFee; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public view override(IERC165, BaseMinterV2_1) returns (bool) { + return + BaseMinterV2_1.supportsInterface(interfaceId) || interfaceId == type(IRangeEditionMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IMinterModuleV2_1 + */ + function moduleInterfaceId() public pure returns (bytes4) { + return type(IRangeEditionMinterV2_1).interfaceId; + } + + /** + * @inheritdoc IRangeEditionMinterV2_1 + */ + function isV2_1() external pure override returns (bool) { + return true; + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + /** + * @dev Restricts the `startTime` to be less than `cutoffTime`, + * and `cutoffTime` to be less than `endTime`. + * @param startTime The start unix timestamp of the mint. + * @param cutoffTime The cutoff unix timestamp of the mint. + * @param endTime The end unix timestamp of the mint. + */ + function _requireValidCombinedTimeRange( + uint32 startTime, + uint32 cutoffTime, + uint32 endTime + ) internal pure { + if (!(startTime < cutoffTime && cutoffTime < endTime)) revert InvalidTimeRange(); + } + + /** + * @dev Gets the current maximum mintable quantity. + * @param data The edition mint data. + * @return The computed value. + */ + function _getMaxMintable(EditionMintData storage data) internal view returns (uint32) { + uint32 _maxMintable; + if (block.timestamp < data.cutoffTime) { + _maxMintable = data.maxMintableUpper; + } else { + _maxMintable = data.maxMintableLower; + } + return _maxMintable; + } +} diff --git a/contracts/modules/SAMV1_1.sol b/contracts/modules/SAMV1_1.sol new file mode 100644 index 00000000..6b0f9a60 --- /dev/null +++ b/contracts/modules/SAMV1_1.sol @@ -0,0 +1,945 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.16; + +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; +import { ISAMV1_1, SAMInfo } from "./interfaces/ISAMV1_1.sol"; +import { ISoundCreatorV1 } from "@core/interfaces/ISoundCreatorV1.sol"; +import { BondingCurveLib } from "./utils/BondingCurveLib.sol"; +import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; + +/* + * @title SAM + * @notice Module for Sound automated market. + * @author Sound.xyz + */ +contract SAMV1_1 is ISAMV1_1, Ownable { + // ============================================================= + // CONSTANTS + // ============================================================= + + /** + * @dev This is the denominator, in basis points (BPS), for any of the fees. + */ + uint16 public constant BPS_DENOMINATOR = 10_000; + + /** + * @dev The maximum basis points (BPS) limit allowed for the platform fees. + */ + uint16 public constant MAX_PLATFORM_FEE_BPS = 500; + + /** + * @dev The maximum basis points (BPS) limit allowed for the artist fees. + */ + uint16 public constant MAX_ARTIST_FEE_BPS = 1_000; + + /** + * @dev The maximum basis points (BPS) limit allowed for the affiliate fees. + */ + uint16 public constant MAX_AFFILIATE_FEE_BPS = 500; + + /** + * @dev The maximum basis points (BPS) limit allowed for the golden egg fees. + */ + uint16 public constant MAX_GOLDEN_EGG_FEE_BPS = 500; + + /** + * @dev The maximum platform per-transaction flat fee. + */ + uint96 public constant MAX_PLATFORM_PER_TX_FLAT_FEE = 0.1 ether; + + /** + * @dev The interface ID for SAM v1.0. + */ + bytes4 private constant _INTERFACE_ID_SAM_V1_0 = 0xa3c2dbc7; + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev How much platform fees have been accrued. + */ + uint128 public platformFeesAccrued; + + /** + * @dev The platform per-transaction flat fee. + */ + uint96 public platformPerTxFlatFee; + + /** + * @dev The platform fee in basis points. + */ + uint16 public platformFeeBPS; + + /** + * @dev Just in case. Won't cost much overhead anyway since it is packed. + */ + bool internal _reentrancyGuard; + + /** + * @dev The platform fee address. + */ + address public platformFeeAddress; + + /** + * @dev List of approved edition factories. + */ + address[] internal _approvedEditionFactories; + + /** + * @dev The data for the sound automated markets. + * edition => SAMData + */ + mapping(address => SAMData) internal _samData; + + /** + * @dev Maps an address to how much affiliate fees have they accrued. + */ + mapping(address => uint128) public affiliateFeesAccrued; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor() payable { + _initializeOwner(msg.sender); + } + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISAMV1_1 + */ + function create( + address edition, + uint96 basePrice, + uint128 linearPriceSlope, + uint128 inflectionPrice, + uint32 inflectionPoint, + uint32 maxSupply, + uint32 buyFreezeTime, + uint16 artistFeeBPS, + uint16 goldenEggFeeBPS, + uint16 affiliateFeeBPS, + address editionBy, + bytes32 editionSalt + ) public { + // We don't use modifiers here in order to prevent stack too deep. + _requireOnlyEditionOwnerOrAdmin(edition); // `onlyEditionOwnerOrAdmin`. + _requireOnlyBeforeSAMPhase(edition); // `onlyBeforeSAMPhase`. + if (maxSupply == 0) revert InvalidMaxSupply(); + if (buyFreezeTime == 0) revert InvalidBuyFreezeTime(); + if (artistFeeBPS > MAX_ARTIST_FEE_BPS) revert InvalidArtistFeeBPS(); + if (goldenEggFeeBPS > MAX_GOLDEN_EGG_FEE_BPS) revert InvalidGoldenEggFeeBPS(); + if (affiliateFeeBPS > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS(); + + _requireEditionIsApproved(edition, editionBy, editionSalt); + + SAMData storage data = _samData[edition]; + + if (data.created) revert SAMAlreadyExists(); + + data.basePrice = basePrice; + data.linearPriceSlope = linearPriceSlope; + data.inflectionPrice = inflectionPrice; + data.inflectionPoint = inflectionPoint; + data.maxSupply = maxSupply; + data.buyFreezeTime = buyFreezeTime; + data.artistFeeBPS = artistFeeBPS; + data.goldenEggFeeBPS = goldenEggFeeBPS; + data.affiliateFeeBPS = affiliateFeeBPS; + data.created = true; + + emit Created( + edition, + basePrice, + linearPriceSlope, + inflectionPrice, + inflectionPoint, + maxSupply, + buyFreezeTime, + artistFeeBPS, + goldenEggFeeBPS, + affiliateFeeBPS + ); + } + + /** + * For avoiding stack too deep. + */ + struct _BuyTemps { + uint256 fromCurveSupply; + uint256 fromTokenId; + uint256 requiredEtherValue; + uint256 subTotal; + uint256 platformFee; + uint256 artistFee; + uint256 goldenEggFee; + uint256 affiliateFee; + uint256 quantity; + address affiliate; + bool affiliated; + } + + /** + * @inheritdoc ISAMV1_1 + */ + function buy( + address edition, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributonId + ) public payable nonReentrant { + if (quantity == 0) revert MintZeroQuantity(); + + _BuyTemps memory t; + SAMData storage data = _getSAMData(edition); + t.quantity = quantity; // Cache the `quantity` to avoid stack too deep. + t.affiliate = affiliate; // Cache the `affiliate` to avoid stack too deep. + t.fromCurveSupply = data.supply; // Cache the `data.supply`. + + if (block.timestamp >= data.buyFreezeTime) revert BuyIsFrozen(); + + ( + t.requiredEtherValue, + t.subTotal, + t.platformFee, + t.artistFee, + t.goldenEggFee, + t.affiliateFee + ) = _totalBuyPriceAndFees(data, uint32(t.fromCurveSupply), quantity); + + if (msg.value < t.requiredEtherValue) revert Underpaid(msg.value, t.requiredEtherValue); + + unchecked { + // Check if the purchase won't exceed the supply cap. + if (t.fromCurveSupply + t.quantity > data.maxSupply) { + revert ExceedsMaxSupply(uint32(data.maxSupply - t.fromCurveSupply)); + } + + // Check if the affiliate is actually affiliated for edition with the affiliate proof. + t.affiliated = isAffiliatedWithProof(edition, affiliate, affiliateProof); + // If affiliated, compute and accrue the affiliate fee. + if (t.affiliated) { + // Accrue the affiliate fee. + if (t.affiliateFee != 0) { + affiliateFeesAccrued[affiliate] = SafeCastLib.toUint128( + uint256(affiliateFeesAccrued[affiliate]) + t.affiliateFee + ); + } + } else { + // If the affiliate is not the zero address despite not being + // affiliated, it might be due to an invalid affiliate proof. + // Revert to prevent redirection of fees. + if (affiliate != address(0)) { + revert InvalidAffiliate(); + } + // Otherwise, redirect the affiliate fee to the artist fee instead. + t.artistFee += t.affiliateFee; + t.affiliateFee = 0; + } + + // Accrue the platform fee. + if (t.platformFee != 0) { + platformFeesAccrued = SafeCastLib.toUint128(uint256(platformFeesAccrued) + t.platformFee); + } + + // Accrue the golden egg fee. + if (t.goldenEggFee != 0) { + data.goldenEggFeesAccrued = SafeCastLib.toUint112(uint256(data.goldenEggFeesAccrued) + t.goldenEggFee); + } + + // Add the `subTotal` to the balance. + data.balance = SafeCastLib.toUint112(uint256(data.balance) + t.subTotal); + + // Add `quantity` to the supply. + data.supply = SafeCastLib.toUint32(t.fromCurveSupply + t.quantity); + + // Indicate that tokens have already been minted via the bonding curve. + data.hasMinted = true; + + // Mint the tokens and transfer the artist fee to the edition contract. + t.fromTokenId = ISoundEditionV1_2(edition).samMint{ value: t.artistFee }(to, quantity); + + // Refund any excess ETH. + if (msg.value > t.requiredEtherValue) { + SafeTransferLib.forceSafeTransferETH(msg.sender, msg.value - t.requiredEtherValue); + } + + emit Bought( + edition, + to, + t.fromTokenId, + uint32(t.fromCurveSupply), + uint32(t.quantity), + uint128(t.requiredEtherValue), + uint128(t.platformFee), + uint128(t.artistFee), + uint128(t.goldenEggFee), + uint128(t.affiliateFee), + t.affiliate, + t.affiliated, + attributonId + ); + } + } + + /** + * @inheritdoc ISAMV1_1 + */ + function sell( + address edition, + uint256[] calldata tokenIds, + uint256 minimumPayout, + address payoutTo, + uint256 attributonId + ) public nonReentrant { + uint256 quantity = tokenIds.length; + // To prevent no-op. + if (quantity == 0) revert BurnZeroQuantity(); + + unchecked { + SAMData storage data = _getSAMData(edition); + + uint256 supply = data.supply; + + // Revert with `InsufficientSupply(available, required)` if `supply < quantity`. + if (supply < quantity) revert InsufficientSupply(supply, quantity); + // Will not underflow because of the above check. + uint256 supplyMinusQuantity = supply - quantity; + + // Compute how much to pay out. + uint256 payout = _subTotal(data, uint32(supplyMinusQuantity), uint32(quantity)); + // Revert if the payout isn't sufficient. + if (payout < minimumPayout) revert InsufficientPayout(payout, minimumPayout); + + // Decrease the supply. + data.supply = uint32(supplyMinusQuantity); + + // Deduct `payout` from `data.balance`. + uint256 balance = data.balance; + // Second safety guard. If we actually revert here, something is wrong. + if (balance < payout) revert("WTF"); + // Will not underflow because of the above check. + data.balance = uint112(balance - payout); + + // Burn the tokens. + ISoundEditionV1_2(edition).samBurn(msg.sender, tokenIds); + + // Pay out the ETH. + SafeTransferLib.forceSafeTransferETH(payoutTo, payout); + + emit Sold(edition, payoutTo, uint32(supply), tokenIds, uint128(payout), attributonId); + } + } + + // Bonding curve price parameter setters: + // -------------------------------------- + // The following functions can only be called before the SAM phase: + // - Before the mint has concluded on the SoundEdition. + // - Before `_samData[edition].hasMinted` is set to true. + // + // Once any tokens have been minted via SAM, + // these setters cannot be called. + // + // These parameters must be unchangable during the SAM + // phase to ensure the consistency between the buy and sell prices. + + /** + * @inheritdoc ISAMV1_1 + */ + function setBasePrice(address edition, uint96 basePrice) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.basePrice = basePrice; + emit BasePriceSet(edition, basePrice); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setLinearPriceSlope(address edition, uint128 linearPriceSlope) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.linearPriceSlope = linearPriceSlope; + emit LinearPriceSlopeSet(edition, linearPriceSlope); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setInflectionPrice(address edition, uint128 inflectionPrice) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.inflectionPrice = inflectionPrice; + emit InflectionPriceSet(edition, inflectionPrice); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setInflectionPoint(address edition, uint32 inflectionPoint) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.inflectionPoint = inflectionPoint; + emit InflectionPointSet(edition, inflectionPoint); + } + + // Per edition fee BPS setters: + // ---------------------------- + // To provide flexbility, we allow the artist to adjust the fees + // even during the SAM phase. As these BPSes cannot exceed hardcoded limits, + // in the event that am artist account is compromised, the worse case is + // users having to pay the maximum limits on the fees. + // + // Note: The golden egg fee setter is given special treatment: + // it cannot be called once the mint has concluded on + // SoundEdition or if any tokens have been minted. + + /** + * @inheritdoc ISAMV1_1 + */ + function setArtistFee(address edition, uint16 bps) public onlyEditionOwnerOrAdmin(edition) { + SAMData storage data = _getSAMData(edition); + if (bps > MAX_ARTIST_FEE_BPS) revert InvalidArtistFeeBPS(); + data.artistFeeBPS = bps; + emit ArtistFeeSet(edition, bps); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setGoldenEggFee(address edition, uint16 bps) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + if (bps > MAX_GOLDEN_EGG_FEE_BPS) revert InvalidGoldenEggFeeBPS(); + data.goldenEggFeeBPS = bps; + emit GoldenEggFeeSet(edition, bps); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setAffiliateFee(address edition, uint16 bps) public onlyEditionOwnerOrAdmin(edition) { + SAMData storage data = _getSAMData(edition); + if (bps > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS(); + data.affiliateFeeBPS = bps; + emit AffiliateFeeSet(edition, bps); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setAffiliateMerkleRoot(address edition, bytes32 root) public onlyEditionOwnerOrAdmin(edition) { + // Note that we want to allow adding a root even while the bonding curve + // is still ongoing, in case the need to prevent spam arises. + + SAMData storage data = _getSAMData(edition); + data.affiliateMerkleRoot = root; + emit AffiliateMerkleRootSet(edition, root); + } + + // Other per edition setters: + // -------------------------- + // To provide flexbility, we allow the artist to adjust these parameters + // even during the SAM phase. + // + // These functions are unable to inflate the supply during the SAM phase. + + /** + * @inheritdoc ISAMV1_1 + */ + function setMaxSupply(address edition, uint32 maxSupply) public onlyEditionOwnerOrAdmin(edition) { + SAMData storage data = _getSAMData(edition); + // Disallow increasing during the SAM phase. + if (maxSupply > data.maxSupply) + if (_inSAMPhase(edition)) revert InvalidMaxSupply(); + data.maxSupply = maxSupply; + emit MaxSupplySet(edition, maxSupply); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setBuyFreezeTime(address edition, uint32 buyFreezeTime) public onlyEditionOwnerOrAdmin(edition) { + SAMData storage data = _getSAMData(edition); + // Disallow increasing during the SAM phase. + if (buyFreezeTime > data.buyFreezeTime) + if (_inSAMPhase(edition)) revert InvalidBuyFreezeTime(); + data.buyFreezeTime = buyFreezeTime; + emit BuyFreezeTimeSet(edition, buyFreezeTime); + } + + // Withdrawal functions: + // --------------------- + // These functions can be called by anyone. + + /** + * @inheritdoc ISAMV1_1 + */ + function withdrawForAffiliate(address affiliate) public nonReentrant { + uint128 accrued = affiliateFeesAccrued[affiliate]; + if (accrued != 0) { + affiliateFeesAccrued[affiliate] = 0; + SafeTransferLib.forceSafeTransferETH(affiliate, accrued); + emit AffiliateFeesWithdrawn(affiliate, accrued); + } + } + + /** + * @inheritdoc ISAMV1_1 + */ + function withdrawForPlatform() public nonReentrant { + address to = platformFeeAddress; + if (to == address(0)) revert PlatformFeeAddressIsZero(); + uint128 accrued = platformFeesAccrued; + if (accrued != 0) { + platformFeesAccrued = 0; + SafeTransferLib.forceSafeTransferETH(to, accrued); + emit PlatformFeesWithdrawn(accrued); + } + } + + /** + * @inheritdoc ISAMV1_1 + */ + function withdrawForGoldenEgg(address edition) public nonReentrant { + SAMData storage data = _getSAMData(edition); + uint128 accrued = data.goldenEggFeesAccrued; + if (accrued != 0) { + data.goldenEggFeesAccrued = 0; + address receipient = goldenEggFeeRecipient(edition); + SafeTransferLib.forceSafeTransferETH(receipient, accrued); + emit GoldenEggFeesWithdrawn(edition, receipient, accrued); + } + } + + // Only onwer setters: + // ------------------- + // These functions can only be called by the owner of the SAM contract. + + /** + * @inheritdoc ISAMV1_1 + */ + function setPlatformFee(uint16 bps) public onlyOwner { + if (bps > MAX_PLATFORM_FEE_BPS) revert InvalidPlatformFeeBPS(); + platformFeeBPS = bps; + emit PlatformFeeSet(bps); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setPlatformPerTxFlatFee(uint96 perTxFlatFee) public onlyOwner { + if (perTxFlatFee > MAX_PLATFORM_PER_TX_FLAT_FEE) revert InvalidPlatformPerTxFlatFee(); + platformPerTxFlatFee = perTxFlatFee; + emit PlatformPerTxFlatFeeSet(perTxFlatFee); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setPlatformFeeAddress(address addr) public onlyOwner { + if (addr == address(0)) revert PlatformFeeAddressIsZero(); + platformFeeAddress = addr; + emit PlatformFeeAddressSet(addr); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function setApprovedEditionFactories(address[] calldata factories) public onlyOwner { + _approvedEditionFactories = factories; + emit ApprovedEditionFactoriesSet(factories); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISAMV1_1 + */ + function samInfo(address edition) external view returns (SAMInfo memory info) { + SAMData storage data = _getSAMData(edition); + info.basePrice = data.basePrice; + info.inflectionPrice = data.inflectionPrice; + info.linearPriceSlope = data.linearPriceSlope; + info.inflectionPoint = data.inflectionPoint; + info.goldenEggFeesAccrued = data.goldenEggFeesAccrued; + info.supply = data.supply; + info.balance = data.balance; + info.maxSupply = data.maxSupply; + info.buyFreezeTime = data.buyFreezeTime; + info.artistFeeBPS = data.artistFeeBPS; + info.affiliateFeeBPS = data.affiliateFeeBPS; + info.goldenEggFeeBPS = data.goldenEggFeeBPS; + info.affiliateMerkleRoot = data.affiliateMerkleRoot; + } + + /** + * @inheritdoc ISAMV1_1 + */ + function totalValue( + address edition, + uint32 fromSupply, + uint32 quantity + ) public view returns (uint256 total) { + total = _subTotal(_getSAMData(edition), fromSupply, quantity); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function totalBuyPriceAndFees( + address edition, + uint32 supplyForwardOffset, + uint32 quantity + ) + public + view + returns ( + uint256 total, + uint256 platformFee, + uint256 artistFee, + uint256 goldenEggFee, + uint256 affiliateFee + ) + { + SAMData storage data = _getSAMData(edition); + uint256 fromSupply = uint256(data.supply) + uint256(supplyForwardOffset); + // Reverts if the planned purchase exceeds the supply cap. Just for correctness. + if (fromSupply + uint256(quantity) > data.maxSupply) { + revert ExceedsMaxSupply(uint32(data.maxSupply - fromSupply)); + } + (total, , platformFee, artistFee, goldenEggFee, affiliateFee) = _totalBuyPriceAndFees( + data, + SafeCastLib.toUint32(fromSupply), + quantity + ); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function totalSellPrice( + address edition, + uint32 supplyBackwardOffset, + uint32 quantity + ) public view returns (uint256 total) { + SAMData storage data = _getSAMData(edition); + + // All checked math. Will revert if anything underflows. + uint256 supply = uint256(data.supply) - uint256(supplyBackwardOffset); + uint256 supplyMinusQuantity = supply - uint256(quantity); + + total = _subTotal(data, uint32(supplyMinusQuantity), uint32(quantity)); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function goldenEggFeeRecipient(address edition) public view returns (address recipient) { + // We use assembly because we don't want to revert + // if the `metadataModule` is not a valid metadata module contract. + // Plain solidity requires an extra codesize check. + assembly { + // Initialize the recipient to the edition by default. + recipient := edition + // Store the function selector of `metadataModule()`. + mstore(0x00, 0x3684d100) + + if iszero(and(eq(returndatasize(), 0x20), staticcall(gas(), edition, 0x1c, 0x04, 0x00, 0x20))) { + // For better gas estimation, and to require that edition + // is a contract with the `metadataModule()` function. + revert(0, 0) + } + + let metadataModule := mload(0x00) + // Store the function selector of `getGoldenEggTokenId(address)`. + mstore(0x00, 0x4baca2b5) + mstore(0x20, edition) + + let success := staticcall(gas(), metadataModule, 0x1c, 0x24, 0x20, 0x20) + if iszero(success) { + // If there is no returndata upon revert, + // it is likely due to an out-of-gas error. + if iszero(returndatasize()) { + revert(0, 0) // For better gas estimation. + } + } + + if and(eq(returndatasize(), 0x20), success) { + // Store the function selector of `ownerOf(uint256)`. + mstore(0x00, 0x6352211e) + // The `goldenEggTokenId` is already in slot 0x20, + // as the previous staticcall directly writes the output to slot 0x20. + + success := staticcall(gas(), edition, 0x1c, 0x24, 0x00, 0x20) + if iszero(success) { + // If there is no returndata upon revert, + // it is likely due to an out-of-gas error. + if iszero(returndatasize()) { + revert(0, 0) // For better gas estimation. + } + } + + if and(eq(returndatasize(), 0x20), success) { + recipient := mload(0x00) + } + } + } + } + + /** + * @inheritdoc ISAMV1_1 + */ + function goldenEggFeesAccrued(address edition) public view returns (uint128) { + return _getSAMData(edition).goldenEggFeesAccrued; + } + + /** + * @inheritdoc ISAMV1_1 + */ + function isAffiliatedWithProof( + address edition, + address affiliate, + bytes32[] calldata affiliateProof + ) public view returns (bool) { + bytes32 root = _getSAMData(edition).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 + affiliate != address(0) && + MerkleProofLib.verifyCalldata(affiliateProof, root, keccak256(abi.encodePacked(affiliate))); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function isAffiliated(address edition, address affiliate) public view returns (bool) { + return isAffiliatedWithProof(edition, affiliate, MerkleProofLib.emptyProof()); + } + + /** + * @inheritdoc ISAMV1_1 + */ + function affiliateMerkleRoot(address edition) external view returns (bytes32) { + return _getSAMData(edition).affiliateMerkleRoot; + } + + /** + * @inheritdoc ISAMV1_1 + */ + function approvedEditionFactories() external view returns (address[] memory) { + return _approvedEditionFactories; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) public pure override(IERC165) returns (bool) { + return + interfaceId == this.supportsInterface.selector || + interfaceId == _INTERFACE_ID_SAM_V1_0 || + interfaceId == type(ISAMV1_1).interfaceId; + } + + /** + * @inheritdoc ISAMV1_1 + */ + function moduleInterfaceId() public pure returns (bytes4) { + return type(ISAMV1_1).interfaceId; + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + /** + * @dev Requires that the caller is the owner or admin of `edition`. + * @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.sender(); + if (sender != OwnableRoles(edition).owner()) + if (!OwnableRoles(edition).hasAnyRole(sender, ISoundEditionV1_2(edition).ADMIN_ROLE())) + revert Unauthorized(); + } + + /** + * @dev Guards the function from reentrancy. + */ + modifier nonReentrant() { + require(_reentrancyGuard == false); + _reentrancyGuard = true; + _; + _reentrancyGuard = false; + } + + /** + * @dev Requires that the `edition` is not in SAM phase. + * @param edition The edition address. + */ + modifier onlyBeforeSAMPhase(address edition) { + _requireOnlyBeforeSAMPhase(edition); + _; + } + + /** + * @dev Requires that the `edition` is not in SAM phase. + * @param edition The edition address. + */ + function _requireOnlyBeforeSAMPhase(address edition) internal view { + if (_inSAMPhase(edition)) revert InSAMPhase(); + } + + /** + * @dev Returns whether the edition is in SAM phase. + * @param edition The edition address. + * @return result Whether the edition has any minted via SAM, or has initial mints concluded. + */ + function _inSAMPhase(address edition) internal view returns (bool result) { + // As long as one token has been bought on the bonding curve, + // the initial mints have already concluded. This `hasMinted` check + // disallows a spoofed `mintConcluded` from changing the curve parameters. + result = _samData[edition].hasMinted || ISoundEditionV1_2(edition).mintConcluded(); + } + + /** + * @dev Returns the storage pointer to the SAMData for `edition`. + * Reverts if the Sound Automated Market does not exist. + * @param edition The edition address. + * @return data Storage pointer to a SAMData. + */ + function _getSAMData(address edition) internal view returns (SAMData storage data) { + data = _samData[edition]; + if (!data.created) revert SAMDoesNotExist(); + } + + /** + * @dev Returns the area under the bonding curve, which is the price before any fees. + * @param data Storage pointer to a SAMData. + * @param fromSupply The starting SAM supply. + * @param quantity The number of tokens to be minted. + * @return subTotal The area under the bonding curve. + */ + function _subTotal( + SAMData storage data, + uint32 fromSupply, + uint32 quantity + ) internal view returns (uint256 subTotal) { + unchecked { + subTotal = uint256(data.basePrice) * uint256(quantity); + subTotal += BondingCurveLib.linearSum(data.linearPriceSlope, fromSupply, quantity); + subTotal += BondingCurveLib.sigmoid2Sum(data.inflectionPoint, data.inflectionPrice, fromSupply, quantity); + } + } + + /** + * @dev Returns the total buy price and the fee per BPS. + * @param data Storage pointer to a SAMData. + * @param fromSupply The starting SAM supply. + * @param quantity The number of tokens to be minted. + * @return total The total buy price with fees. + * @return subTotal The buy price before fees. + * @return platformFee The platform fee. + * @return artistFee The artist fee. + * @return goldenEggFee The golden egg fee. + * @return affiliateFee The affiliate fee. + */ + function _totalBuyPriceAndFees( + SAMData storage data, + uint32 fromSupply, + uint32 quantity + ) + internal + view + returns ( + uint256 total, + uint256 subTotal, + uint256 platformFee, + uint256 artistFee, + uint256 goldenEggFee, + uint256 affiliateFee + ) + { + unchecked { + subTotal = _subTotal(data, fromSupply, quantity); + + uint256 feePerBPS = FixedPointMathLib.rawDiv(subTotal, BPS_DENOMINATOR); + + platformFee = uint256(platformFeeBPS) * feePerBPS + uint256(platformPerTxFlatFee); + artistFee = uint256(data.artistFeeBPS) * feePerBPS; + goldenEggFee = uint256(data.goldenEggFeeBPS) * feePerBPS; + affiliateFee = uint256(data.affiliateFeeBPS) * feePerBPS; + + total = subTotal + platformFee + artistFee + goldenEggFee + affiliateFee; + } + } + + /** + * @dev Reverts if the `edition` is not created by an approved factory. + * @param edition The edition address. + * @param by The address which created the edition via the factory. + * @param salt The salt used to create the edition via the factory. + */ + function _requireEditionIsApproved( + address edition, + address by, + bytes32 salt + ) internal view virtual { + uint256 n = _approvedEditionFactories.length; + unchecked { + // As long as there is one approved factory that states that it has + // created the `edition`, we return from the function, instead of reverting. + for (uint256 i; i != n; ++i) { + address factory = _approvedEditionFactories[i]; + try ISoundCreatorV1(factory).soundEditionAddress(by, salt) returns (address addr, bool) { + if (addr == edition) return; + } catch {} + } + } + revert UnapprovedEdition(); + } +} diff --git a/contracts/modules/interfaces/IEditionMaxMinterV2_1.sol b/contracts/modules/interfaces/IEditionMaxMinterV2_1.sol new file mode 100644 index 00000000..d3e2be24 --- /dev/null +++ b/contracts/modules/interfaces/IEditionMaxMinterV2_1.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; + +/** + * @dev All the information about a edition max mint. + */ +struct MintInfo { + uint32 startTime; + uint32 endTime; + uint16 affiliateFeeBPS; + bool mintPaused; + uint96 price; + uint32 maxMintableLower; + uint32 maxMintableUpper; + uint32 maxMintablePerAccount; + uint32 totalMinted; + uint32 cutoffTime; + bytes32 affiliateMerkleRoot; + uint16 platformFeeBPS; + uint96 platformFlatFee; + uint96 platformPerTxFlatFee; +} + +/** + * @title IEditionMaxMinterV2_1 + * @dev Interface for the `EditionMaxMinterV2_1` module. + * @author Sound.xyz + */ +interface IEditionMaxMinterV2_1 is IMinterModuleV2_1 { + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when a edition max is created. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + * @param startTime Start timestamp of sale (in seconds since unix epoch). + * @param endTime End timestamp of sale (in seconds since unix epoch). + * @param affiliateFeeBPS The affiliate fee in basis points. + * @param maxMintablePerAccount The maximum number of tokens that can be minted per account. + */ + event EditionMaxMintCreated( + address indexed edition, + uint128 mintId, + uint96 price, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintablePerAccount + ); + + /** + * @dev Emitted when the `price` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + event PriceSet(address indexed edition, uint128 mintId, uint96 price); + + /** + * @dev Emitted when the `maxMintablePerAccount` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintablePerAccount The maximum number of tokens that can be minted per account. + */ + event MaxMintablePerAccountSet(address indexed edition, uint128 mintId, uint32 maxMintablePerAccount); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The number of tokens minted has exceeded the number allowed for each account. + */ + error ExceedsMaxPerAccount(); + + /** + * @dev The max mintable per account cannot be zero. + */ + error MaxMintablePerAccountIsZero(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Initializes a range mint instance + * @param edition Address of the song edition contract we are minting for. + * @param price Sale price in ETH for minting a single token in `edition`. + * @param startTime Start timestamp of sale (in seconds since unix epoch). + * @param endTime End timestamp of sale (in seconds since unix epoch). + * @param affiliateFeeBPS The affiliate fee in basis points. + * @param maxMintablePerAccount The maximum number of tokens that can be minted by an account. + * @return mintId The ID for the new mint instance. + */ + function createEditionMint( + address edition, + uint96 price, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintablePerAccount + ) external returns (uint128 mintId); + + /** + * @dev Mints tokens for a given edition. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param to The address to mint to. + * @param quantity Token quantity to mint in song `edition`. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @param attributionId The attribution ID. + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) external payable; + + /** + * @dev Mints tokens for a given edition. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param quantity Token quantity to mint in song `edition`. + * @param affiliate The affiliate address. + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + address affiliate + ) external payable; + + /** + * @dev Sets the `price` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) external; + + /** + * @dev Sets the `maxMintablePerAccount` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintablePerAccount The maximum number of tokens that can be minted by an account. + */ + function setMaxMintablePerAccount( + address edition, + uint128 mintId, + uint32 maxMintablePerAccount + ) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns {IEditionMaxMinterV2_1.MintInfo} instance containing the full minter parameter set. + * @param edition The edition to get the mint instance for. + * @param mintId The ID of the mint instance. + * @return mintInfo Information about this mint. + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory); + + /** + * @dev To prevent ERC165 selector collision. + */ + function isV2_1() external pure returns (bool); +} diff --git a/contracts/modules/interfaces/IFixedPriceSignatureMinterV2_1.sol b/contracts/modules/interfaces/IFixedPriceSignatureMinterV2_1.sol new file mode 100644 index 00000000..17a3c42f --- /dev/null +++ b/contracts/modules/interfaces/IFixedPriceSignatureMinterV2_1.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; + +/** + * @dev Data unique to a fixed-price signature mint. + */ +struct EditionMintData { + // Whitelist signer address. + address signer; + // The maximum number of tokens that can can be minted for this sale. + uint32 maxMintable; + // The total number of tokens minted so far for this sale. + uint32 totalMinted; +} + +/** + * @dev All the information about a fixed-price signature mint (combines EditionMintData with BaseData). + */ +struct MintInfo { + uint32 startTime; + uint32 endTime; + uint16 affiliateFeeBPS; + bool mintPaused; + uint96 price; + uint32 maxMintable; + uint32 maxMintablePerAccount; + uint32 totalMinted; + address signer; + bytes32 affiliateMerkleRoot; + uint16 platformFeeBPS; + uint96 platformFlatFee; + uint96 platformPerTxFlatFee; +} + +/** + * @title IFixedPriceSignatureMinterV2_1 + * @dev Interface for the `FixedPriceSignatureMinterV2_1` module. + * @author Sound.xyz + */ +interface IFixedPriceSignatureMinterV2_1 is IMinterModuleV2_1 { + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when a new fixed price signature mint is created. + * @param edition The edition address. + * @param mintId The mint ID. + * @param signer The address of the signer that authorizes mints. + * @param maxMintable The maximum number of tokens that can be minted. + * @param startTime The time minting can begin. + * @param endTime The time minting will end. + * @param affiliateFeeBPS The affiliate fee in basis points. + */ + event FixedPriceSignatureMintCreated( + address indexed edition, + uint128 mintId, + uint96 price, + address signer, + uint32 maxMintable, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS + ); + + /** + * @dev Emitted when the `maxMintable` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintable The maximum number of tokens that can be minted on this schedule. + */ + event MaxMintableSet(address indexed edition, uint128 mintId, uint32 maxMintable); + + /** + * @dev Emitted when the `price` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + event PriceSet(address indexed edition, uint128 mintId, uint96 price); + + /** + * @dev Emitted when the `signer` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param signer The address of the signer that authorizes mints. + */ + event SignerSet(address indexed edition, uint128 mintId, address signer); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev Cannot mint more than the signed quantity. + */ + error ExceedsSignedQuantity(); + + /** + * @dev The signature is invalid. + */ + error InvalidSignature(); + + /** + * @dev The mint sigature can only be used a single time. + */ + error SignatureAlreadyUsed(); + + /** + * @dev The signer can't be the zero address. + */ + error SignerIsZeroAddress(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Initializes a fixed-price signature mint instance. + * @param edition The edition address. + * @param price The price to mint a token. + * @param signer The address of the signer that authorizes mints. + * @param maxMintable_ The maximum number of tokens that can be minted. + * @param startTime The time minting can begin. + * @param endTime The time minting will end. + * @param affiliateFeeBPS The affiliate fee in basis points. + * @return mintId The ID of the new mint instance. + */ + function createEditionMint( + address edition, + uint96 price, + address signer, + uint32 maxMintable_, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS + ) external returns (uint128 mintId); + + /** + * @dev Mints a token for a particular mint instance. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param to The address to mint to. + * @param quantity The quantity of tokens to mint. + * @param signedQuantity The max quantity this buyer has been approved to mint. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @param signature The signed message to authorize the mint. + * @param claimTicket The ticket number to enforce single-use of the signature. + * @param attributionId The attribution ID. + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + uint32 signedQuantity, + address affiliate, + bytes32[] calldata affiliateProof, + bytes calldata signature, + uint32 claimTicket, + uint256 attributionId + ) external payable; + + /** + * @dev Mints a token for a particular mint instance. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param quantity The quantity of tokens to mint. + * @param signedQuantity The max quantity this buyer has been approved to mint. + * @param affiliate The affiliate address. + * @param signature The signed message to authorize the mint. + * @param claimTicket The ticket number to enforce single-use of the signature. + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + uint32 signedQuantity, + address affiliate, + bytes calldata signature, + uint32 claimTicket + ) external payable; + + /** + * @dev Sets the `maxMintable` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintable The maximum number of tokens that can be minted on this schedule. + */ + function setMaxMintable( + address edition, + uint128 mintId, + uint32 maxMintable + ) external; + + /** + * @dev Sets the `price` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) external; + + /** + * @dev Sets the `maxMintablePerAccount` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param signer The address of the signer that authorizes mints. + */ + function setSigner( + address edition, + uint128 mintId, + address signer + ) external; + + // ============================================================= + // PUBLIC / EXTERNAL READ FUNCTIONS + // ============================================================= + + /** + * @dev Returns the EIP-712 type hash of the signature for minting. + * @return typeHash The constant value. + */ + function MINT_TYPEHASH() external view returns (bytes32 typeHash); + + /** + * @dev Returns IFixedPriceSignatureMinterV2_1.MintInfo instance containing the full minter parameter set. + * @param edition The edition to get the mint instance for. + * @param mintId The ID of the mint instance. + * @return Information about this mint. + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory); + + /** + * @dev Returns an array of booleans on whether each claim ticket has been claimed. + * @param edition The edition to get the mint instance for. + * @param mintId The ID of the mint instance. + * @param claimTickets The claim tickets to check. + * @return claimed The computed values. + */ + function checkClaimTickets( + address edition, + uint128 mintId, + uint32[] calldata claimTickets + ) external view returns (bool[] memory claimed); + + /** + * @dev Returns the EIP-712 domain separator of the signature for minting. + * @return separator The constant value. + */ + function DOMAIN_SEPARATOR() external view returns (bytes32 separator); + + /** + * @dev To prevent ERC165 selector collision. + */ + function isV2_1() external pure returns (bool); +} diff --git a/contracts/modules/interfaces/IMerkleDropMinterV2_1.sol b/contracts/modules/interfaces/IMerkleDropMinterV2_1.sol new file mode 100644 index 00000000..21c33b3a --- /dev/null +++ b/contracts/modules/interfaces/IMerkleDropMinterV2_1.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; + +/** + * @dev Data unique to a Merkle drop mint. + */ +struct EditionMintData { + // Hash of the root node for the Merkle tree drop + bytes32 merkleRootHash; + // The maximum number of tokens that can can be minted for this sale. + uint32 maxMintable; + // The total number of tokens minted so far for this sale. + uint32 totalMinted; +} + +/** + * @dev All the information about a Merkle drop mint (combines EditionMintData with BaseData). + */ +struct MintInfo { + uint32 startTime; + uint32 endTime; + uint16 affiliateFeeBPS; + bool mintPaused; + uint96 price; + uint32 maxMintable; + uint32 maxMintablePerAccount; + uint32 totalMinted; + bytes32 merkleRootHash; + bytes32 affiliateMerkleRoot; + uint16 platformFeeBPS; + uint96 platformFlatFee; + uint96 platformPerTxFlatFee; +} + +/** + * @title IMerkleDropMinterV2_1 + * @dev Interface for the `MerkleDropMinterV2_1` module. + * @author Sound.xyz + */ +interface IMerkleDropMinterV2_1 is IMinterModuleV2_1 { + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when a new Merkle drop mint is created. + * @param edition The edition address. + * @param mintId The mint ID. + * @param merkleRootHash The root of the Merkle tree of the approved addresses. + * @param price The price at which each token will be sold, in ETH. + * @param startTime The time minting can begin. + * @param endTime The time minting will end. + * @param affiliateFeeBPS The affiliate fee in basis points. + * @param maxMintable The maximum number of tokens that can be minted. + * @param maxMintablePerAccount The maximum number of tokens that an account can mint. + */ + event MerkleDropMintCreated( + address indexed edition, + uint128 mintId, + bytes32 merkleRootHash, + uint96 price, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintable, + uint32 maxMintablePerAccount + ); + + /** + * @dev Emitted when tokens are claimed by an account. + * @param allowlisted The address in the allowlist. + * @param quantity The quantity of tokens claimed. + */ + event DropClaimed(address allowlisted, uint32 quantity); + + /** + * @dev Emitted when the `price` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + event PriceSet(address indexed edition, uint128 mintId, uint96 price); + + /** + * @dev Emitted when the `maxMintable` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintable The maximum number of tokens that can be minted on this schedule. + */ + event MaxMintableSet(address indexed edition, uint128 mintId, uint32 maxMintable); + + /** + * @dev Emitted when the `maxMintablePerAccount` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintablePerAccount The maximum number of tokens that can be minted per account. + */ + event MaxMintablePerAccountSet(address indexed edition, uint128 mintId, uint32 maxMintablePerAccount); + + /** + * @dev Emitted when the `merkleRootHash` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param merkleRootHash The merkle root hash of the entries. + */ + event MerkleRootHashSet(address indexed edition, uint128 mintId, bytes32 merkleRootHash); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The merkle proof is invalid. + */ + error InvalidMerkleProof(); + + /** + * @dev The caller must be delegated by the allowlisted. + */ + error CallerNotDelegated(); + + /** + * @dev The number of tokens minted has exceeded the number allowed for each account. + */ + error ExceedsMaxPerAccount(); + + /** + * @dev The merkle root hash is empty. + */ + error MerkleRootHashIsEmpty(); + + /** + * @dev The max mintable per account cannot be zero. + */ + error MaxMintablePerAccountIsZero(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Initializes Merkle drop mint instance. + * @param edition Address of the song edition contract we are minting for. + * @param merkleRootHash bytes32 hash of the Merkle tree representing eligible mints. + * @param price Sale price in ETH for minting a single token in `edition`. + * @param startTime Start timestamp of sale (in seconds since unix epoch). + * @param endTime End timestamp of sale (in seconds since unix epoch). + * @param affiliateFeeBPS The affiliate fee in basis points. + * @param maxMintable_ The maximum number of tokens that can can be minted for this sale. + * @param maxMintablePerAccount_ The maximum number of tokens that a single account can mint. + * @return mintId The ID of the new mint instance. + */ + function createEditionMint( + address edition, + bytes32 merkleRootHash, + uint96 price, + uint32 startTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintable_, + uint32 maxMintablePerAccount_ + ) external returns (uint128 mintId); + + /** + * @dev Mints a token for a particular mint instance. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param to The address to mint to. + * @param quantity The quantity of tokens to mint. + * @param allowlisted The address authorized to mint the tokens. + * @param proof The Merkle proof. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @param attributionId The attribution ID. + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address allowlisted, + bytes32[] calldata proof, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) external payable; + + /** + * @dev Mints a token for a particular mint instance. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID.. + * @param quantity The quantity of tokens to mint. + * @param proof The Merkle proof. + * @param affiliate The affiliate address. + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + bytes32[] calldata proof, + address affiliate + ) external payable; + + /** + * @dev Sets the `price` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) external; + + /** + * @dev Sets the `maxMintablePerAccount` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintablePerAccount The maximum number of tokens that can be minted by an account. + */ + function setMaxMintablePerAccount( + address edition, + uint128 mintId, + uint32 maxMintablePerAccount + ) external; + + /** + * @dev Sets the `maxMintable` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintable The maximum number of tokens that can be minted on this schedule. + */ + function setMaxMintable( + address edition, + uint128 mintId, + uint32 maxMintable + ) external; + + /** + * @dev Sets the `merkleRootHash` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param merkleRootHash The Merkle root hash of the entries. + */ + function setMerkleRootHash( + address edition, + uint128 mintId, + bytes32 merkleRootHash + ) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the number of tokens minted to address `to`, for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param to The address to mint to. + * @return count The number of tokens minted. + */ + function mintCount( + address edition, + uint128 mintId, + address to + ) external view returns (uint256 count); + + /** + * @dev Returns IMerkleDropMinterV2_1.MintInfo instance containing the full minter parameter set. + * @param edition The edition to get the mint instance for. + * @param mintId The ID of the mint instance. + * @return mintInfo Information about this mint. + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory); + + /** + * @dev To prevent ERC165 selector collision. + */ + function isV2_1() external pure returns (bool); +} diff --git a/contracts/modules/interfaces/IMinterAdapter.sol b/contracts/modules/interfaces/IMinterAdapter.sol index 954f5392..a4382bfd 100644 --- a/contracts/modules/interfaces/IMinterAdapter.sol +++ b/contracts/modules/interfaces/IMinterAdapter.sol @@ -15,7 +15,7 @@ interface IMinterAdapter is IERC165 { /** * @dev Emitted when a mint happens via the minter adapter. - * @param minter The address of the EditionMaxMinterV2 or RangeEditionMinterV2. + * @param minter The address of the EditionMaxMinterV2_1 or RangeEditionMinterV2_1. * @param edition The address of the edition. * @param fromTokenId The starting token ID in the batch minted. * @param quantity The number of tokens minted. @@ -37,7 +37,7 @@ interface IMinterAdapter is IERC165 { /** * @dev Mints tokens for a given edition. - * @param minter The address of the EditionMaxMinterV2 or RangeEditionMinterV2. + * @param minter The address of the EditionMaxMinterV2_1 or RangeEditionMinterV2_1. * @param edition Address of the song edition contract we are minting for. * @param mintId The mint ID. * @param to The address to mint to. diff --git a/contracts/modules/interfaces/IRangeEditionMinterV2_1.sol b/contracts/modules/interfaces/IRangeEditionMinterV2_1.sol new file mode 100644 index 00000000..70e8f73f --- /dev/null +++ b/contracts/modules/interfaces/IRangeEditionMinterV2_1.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; + +/** + * @dev Data unique to a range edition mint. + */ +struct EditionMintData { + // The timestamp (in seconds since unix epoch) after which the + // max amount of tokens mintable will drop from + // `maxMintableUpper` to `maxMintableLower`. + uint32 cutoffTime; + // The total number of tokens minted. Includes permissioned mints. + uint32 totalMinted; + // The lower limit of the maximum number of tokens that can be minted. + uint32 maxMintableLower; + // The upper limit of the maximum number of tokens that can be minted. + uint32 maxMintableUpper; +} + +/** + * @dev All the information about a range edition mint (combines EditionMintData with BaseData). + */ +struct MintInfo { + uint32 startTime; + uint32 endTime; + uint16 affiliateFeeBPS; + bool mintPaused; + uint96 price; + uint32 maxMintableUpper; + uint32 maxMintableLower; + uint32 maxMintablePerAccount; + uint32 totalMinted; + uint32 cutoffTime; + bytes32 affiliateMerkleRoot; + uint16 platformFeeBPS; + uint96 platformFlatFee; + uint96 platformPerTxFlatFee; +} + +/** + * @title IRangeEditionMinterV2_1 + * @dev Interface for the `RangeEditionMinterV2_1` module. + * @author Sound.xyz + */ +interface IRangeEditionMinterV2_1 is IMinterModuleV2_1 { + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when a range edition is created. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + * @param startTime Start timestamp of sale (in seconds since unix epoch). + * @param cutoffTime The timestamp (in seconds since unix epoch) after which the + * max amount of tokens mintable will drop from + * `maxMintableUpper` to `maxMintableLower`. + * @param endTime End timestamp of sale (in seconds since unix epoch). + * @param affiliateFeeBPS The affiliate fee in basis points. + * @param maxMintableLower The lower limit of the maximum number of tokens that can be minted. + * @param maxMintableUpper The upper limit of the maximum number of tokens that can be minted. + */ + event RangeEditionMintCreated( + address indexed edition, + uint128 mintId, + uint96 price, + uint32 startTime, + uint32 cutoffTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintableLower, + uint32 maxMintableUpper, + uint32 maxMintablePerAccount + ); + + event CutoffTimeSet(address indexed edition, uint128 mintId, uint32 cutoffTime); + + /** + * @dev Emitted when the max mintable range is updated. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintableLower The lower limit of the maximum number of tokens that can be minted. + * @param maxMintableUpper The upper limit of the maximum number of tokens that can be minted. + */ + event MaxMintableRangeSet( + address indexed edition, + uint128 mintId, + uint32 maxMintableLower, + uint32 maxMintableUpper + ); + + /** + * @dev Emitted when the `price` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + event PriceSet(address indexed edition, uint128 mintId, uint96 price); + + /** + * @dev Emitted when the `maxMintablePerAccount` is changed for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintablePerAccount The maximum number of tokens that can be minted per account. + */ + event MaxMintablePerAccountSet(address indexed edition, uint128 mintId, uint32 maxMintablePerAccount); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The `maxMintableLower` must not be greater than `maxMintableUpper`. + */ + error InvalidMaxMintableRange(); + + /** + * @dev The number of tokens minted has exceeded the number allowed for each account. + */ + error ExceedsMaxPerAccount(); + + /** + * @dev The max mintable per account cannot be zero. + */ + error MaxMintablePerAccountIsZero(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Initializes a range mint instance + * @param edition Address of the song edition contract we are minting for. + * @param price Sale price in ETH for minting a single token in `edition`. + * @param startTime Start timestamp of sale (in seconds since unix epoch). + * @param cutoffTime The timestamp (in seconds since unix epoch) after which the + * max amount of tokens mintable will drop from + * `maxMintableUpper` to `maxMintableLower`. + * @param endTime End timestamp of sale (in seconds since unix epoch). + * @param affiliateFeeBPS The affiliate fee in basis points. + * @param maxMintableLower The lower limit of the maximum number of tokens that can be minted. + * @param maxMintableUpper The upper limit of the maximum number of tokens that can be minted. + * @param maxMintablePerAccount_ The maximum number of tokens that can be minted by an account. + * @return mintId The ID for the new mint instance. + */ + function createEditionMint( + address edition, + uint96 price, + uint32 startTime, + uint32 cutoffTime, + uint32 endTime, + uint16 affiliateFeeBPS, + uint32 maxMintableLower, + uint32 maxMintableUpper, + uint32 maxMintablePerAccount_ + ) external returns (uint128 mintId); + + /** + * @dev Sets the time range. + * @param edition Address of the song edition contract we are minting for. + * @param startTime Start timestamp of sale (in seconds since unix epoch). + * @param cutoffTime The timestamp (in seconds since unix epoch) after which the + * max amount of tokens mintable will drop from + * `maxMintableUpper` to `maxMintableLower`. + * @param endTime End timestamp of sale (in seconds since unix epoch). + */ + function setTimeRange( + address edition, + uint128 mintId, + uint32 startTime, + uint32 cutoffTime, + uint32 endTime + ) external; + + /** + * @dev Sets the max mintable range. + * @param edition Address of the song edition contract we are minting for. + * @param maxMintableLower The lower limit of the maximum number of tokens that can be minted. + * @param maxMintableUpper The upper limit of the maximum number of tokens that can be minted. + */ + function setMaxMintableRange( + address edition, + uint128 mintId, + uint32 maxMintableLower, + uint32 maxMintableUpper + ) external; + + /** + * @dev Mints tokens for a given edition. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param to The address to mint to. + * @param quantity Token quantity to mint in song `edition`. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @param attributionId The attribution ID. + */ + function mintTo( + address edition, + uint128 mintId, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) external payable; + + /** + * @dev Mints tokens for a given edition. + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param quantity Token quantity to mint in song `edition`. + * @param affiliate The affiliate address. + */ + function mint( + address edition, + uint128 mintId, + uint32 quantity, + address affiliate + ) external payable; + + /** + * @dev Sets the `price` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param price Sale price in ETH for minting a single token in `edition`. + */ + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) external; + + /** + * @dev Sets the `maxMintablePerAccount` for (`edition`, `mintId`). + * @param edition Address of the song edition contract we are minting for. + * @param mintId The mint ID. + * @param maxMintablePerAccount The maximum number of tokens that can be minted by an account. + */ + function setMaxMintablePerAccount( + address edition, + uint128 mintId, + uint32 maxMintablePerAccount + ) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns {IRangeEditionMinterV2_1.MintInfo} instance containing the full minter parameter set. + * @param edition The edition to get the mint instance for. + * @param mintId The ID of the mint instance. + * @return mintInfo Information about this mint. + */ + function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory); + + /** + * @dev To prevent ERC165 selector collision. + */ + function isV2_1() external pure returns (bool); +} diff --git a/contracts/modules/interfaces/ISAMV1_1.sol b/contracts/modules/interfaces/ISAMV1_1.sol new file mode 100644 index 00000000..f8cb9a41 --- /dev/null +++ b/contracts/modules/interfaces/ISAMV1_1.sol @@ -0,0 +1,807 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; + +/** + * @dev Data unique to a Sound Automated Market (i.e. bonding curve mint). + */ +struct SAMInfo { + uint96 basePrice; + uint128 linearPriceSlope; + uint128 inflectionPrice; + uint32 inflectionPoint; + uint128 goldenEggFeesAccrued; + uint128 balance; + uint32 supply; + uint32 maxSupply; + uint32 buyFreezeTime; + uint16 artistFeeBPS; + uint16 affiliateFeeBPS; + uint16 goldenEggFeeBPS; + bytes32 affiliateMerkleRoot; +} + +/** + * @title ISAM + * @dev Interface for the Sound Automated Market module. + * @author Sound.xyz + */ +interface ISAMV1_1 is IERC165 { + // ============================================================= + // STRUCTS + // ============================================================= + + struct SAMData { + // The sigmoid inflection price of the bonding curve. + uint128 inflectionPrice; + // The price added to the bonding curve price. + uint96 basePrice; + // The sigmoid inflection point of the bonding curve. + uint32 inflectionPoint; + // The amount of fees accrued by the golden egg. + uint112 goldenEggFeesAccrued; + // The balance of the pool for the edition. + // 112 bits is enough to represent 5,192,296,858,534,828 ETH. + // At the point of writing, there are 120,479,006 ETH in Ethereum mainnet, + // and 9,050,469,069 MATIC in Polygon PoS chain. + uint112 balance; + // The amount of tokens in the bonding curve. + uint32 supply; + // The slope for the additional linear component to the bonding curve price. + uint128 linearPriceSlope; + // The supply cap for buying tokens. + // Note: The supply can go over the cap if the cap is manually decreased. + uint32 maxSupply; + // The cutoff time for buying tokens. + uint32 buyFreezeTime; + // The fee BPS (basis points) to pay the artist. + uint16 artistFeeBPS; + // The fee BPS (basis points) to pay affiliates. + uint16 affiliateFeeBPS; + // The fee BPS (basis points) to pay the golden egg holder. + uint16 goldenEggFeeBPS; + // Whether a token has already been minted on the bonding curve. + bool hasMinted; + // Whether the SAM has been created. + bool created; + // The affiliate Merkle root, if any. + bytes32 affiliateMerkleRoot; + } + + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when a bonding curve is created. + * @param edition The edition address. + * @param linearPriceSlope The linear price slope of the bonding curve. + * @param inflectionPrice The sigmoid inflection price of the bonding curve. + * @param inflectionPoint The sigmoid inflection point of the bonding curve. + * @param maxSupply The supply cap for buying tokens. + * @param buyFreezeTime The cutoff time for buying tokens. + * @param artistFeeBPS The fee BPS (basis points) to pay the artist. + * @param goldenEggFeeBPS The fee BPS (basis points) to pay the golden egg holder. + * @param affiliateFeeBPS The fee BPS (basis points) to pay affiliates. + */ + event Created( + address indexed edition, + uint96 basePrice, + uint128 linearPriceSlope, + uint128 inflectionPrice, + uint32 inflectionPoint, + uint32 maxSupply, + uint32 buyFreezeTime, + uint16 artistFeeBPS, + uint16 goldenEggFeeBPS, + uint16 affiliateFeeBPS + ); + + /** + * @dev Emitted when tokens are bought from the bonding curve. + * @param edition The edition address. + * @param buyer Address of the buyer. + * @param fromTokenId The starting token ID minted for the batch. + * @param fromCurveSupply The start of the curve supply for the batch. + * @param quantity The number of tokens bought. + * @param totalPayment The total amount of ETH paid. + * @param platformFee The cut paid to the platform. + * @param artistFee The cut paid to the artist. + * @param goldenEggFee The cut paid to the golden egg. + * @param affiliateFee The cut paid to the affiliate. + * @param affiliate The affiliate's address. + * @param affiliated Whether the affiliate is affiliated. + * @param attributionId The attribution ID. + */ + event Bought( + address indexed edition, + address indexed buyer, + uint256 fromTokenId, + uint32 fromCurveSupply, + uint32 quantity, + uint128 totalPayment, + uint128 platformFee, + uint128 artistFee, + uint128 goldenEggFee, + uint128 affiliateFee, + address affiliate, + bool affiliated, + uint256 indexed attributionId + ); + + /** + * @dev Emitted when tokens are sold into the bonding curve. + * @param edition The edition address. + * @param seller Address of the seller. + * @param fromCurveSupply The start of the curve supply for the batch. + * @param tokenIds The token IDs burned. + * @param totalPayout The total amount of ETH paid out. + * @param attributionId The attribution ID. + */ + event Sold( + address indexed edition, + address indexed seller, + uint32 fromCurveSupply, + uint256[] tokenIds, + uint128 totalPayout, + uint256 indexed attributionId + ); + + /** + * @dev Emitted when the `basePrice` is updated. + * @param edition The edition address. + * @param basePrice The price added to the bonding curve price. + */ + event BasePriceSet(address indexed edition, uint96 basePrice); + + /** + * @dev Emitted when the `linearPriceSlope` is updated. + * @param edition The edition address. + * @param linearPriceSlope The linear price slope of the bonding curve. + */ + event LinearPriceSlopeSet(address indexed edition, uint128 linearPriceSlope); + + /** + * @dev Emitted when the `inflectionPrice` is updated. + * @param edition The edition address. + * @param inflectionPrice The sigmoid inflection price of the bonding curve. + */ + event InflectionPriceSet(address indexed edition, uint128 inflectionPrice); + + /** + * @dev Emitted when the `inflectionPoint` is updated. + * @param edition The edition address. + * @param inflectionPoint The sigmoid inflection point of the bonding curve. + */ + event InflectionPointSet(address indexed edition, uint32 inflectionPoint); + + /** + * @dev Emitted when the `artistFeeBPS` is updated. + * @param edition The edition address. + * @param bps The affiliate fee basis points. + */ + event ArtistFeeSet(address indexed edition, uint16 bps); + + /** + * @dev Emitted when the `affiliateFeeBPS` is updated. + * @param edition The edition address. + * @param bps The affiliate fee basis points. + */ + event AffiliateFeeSet(address indexed edition, uint16 bps); + + /** + * @dev Emitted when the Merkle root for an affiliate allow list is updated. + * @param edition The edition address. + * @param root The Merkle root for the affiliate allow list. + */ + event AffiliateMerkleRootSet(address indexed edition, bytes32 root); + + /** + * @dev Emitted when the `goldenEggFeeBPS` is updated. + * @param edition The edition address. + * @param bps The golden egg fee basis points. + */ + event GoldenEggFeeSet(address indexed edition, uint16 bps); + + /** + * @dev Emitted when the `maxSupply` updated. + * @param edition The edition address. + */ + event MaxSupplySet(address indexed edition, uint32 maxSupply); + + /** + * @dev Emitted when the `buyFreezeTime` updated. + * @param edition The edition address. + */ + event BuyFreezeTimeSet(address indexed edition, uint32 buyFreezeTime); + + /** + * @dev Emitted when the `platformFeeBPS` is updated. + * @param bps The platform fee basis points. + */ + event PlatformFeeSet(uint16 bps); + + /** + * @dev Emitted when the `platformPerTxFlatFee` is updated. + * @param perTxFlatFee The platform fee flat fee per transaction. + */ + event PlatformPerTxFlatFeeSet(uint96 perTxFlatFee); + + /** + * @dev Emitted when the `platformFeeAddress` is updated. + * @param addr The platform fee address. + */ + event PlatformFeeAddressSet(address addr); + + /** + * @dev Emitted when the accrued fees for `affiliate` are withdrawn. + * @param affiliate The affiliate address. + * @param accrued The amount of fees withdrawn. + */ + event AffiliateFeesWithdrawn(address indexed affiliate, uint256 accrued); + + /** + * @dev Emitted when the accrued fees for the golden egg of `edition` are withdrawn. + * @param edition The edition address. + * @param receipient The receipient. + * @param accrued The amount of fees withdrawn. + */ + event GoldenEggFeesWithdrawn(address indexed edition, address indexed receipient, uint128 accrued); + + /** + * @dev Emitted when the accrued fees for the platform are withdrawn. + * @param accrued The amount of fees withdrawn. + */ + event PlatformFeesWithdrawn(uint128 accrued); + + /** + * @dev Emitted when the approved factories are set. + * @param factories The list of approved factories. + */ + event ApprovedEditionFactoriesSet(address[] factories); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The Ether value paid is below the value required. + * @param paid The amount sent to the contract. + * @param required The amount required. + */ + error Underpaid(uint256 paid, uint256 required); + + /** + * @dev The Ether value paid out is below the value required. + * @param payout The amount to pau out.. + * @param required The amount required. + */ + error InsufficientPayout(uint256 payout, uint256 required); + + /** + * @dev There is not enough tokens in the Sound Automated Market for selling back. + * @param available The number of tokens in the Sound Automated Market. + * @param required The amount of tokens required. + */ + error InsufficientSupply(uint256 available, uint256 required); + + /** + * @dev Cannot perform the operation during the SAM phase. + */ + error InSAMPhase(); + + /** + * @dev The inflection price cannot be zero. + */ + error InflectionPriceIsZero(); + + /** + * @dev The inflection point cannot be zero. + */ + error InflectionPointIsZero(); + + /** + * @dev The max supply cannot be increased after the SAM has started. + * In the `create` function, the initial max supply cannot be zero. + */ + error InvalidMaxSupply(); + + /** + * @dev The buy freeze time cannot be increased after the SAM has started. + * In the `create` function, the initial buy freeze time cannot be zero. + */ + error InvalidBuyFreezeTime(); + + /** + * @dev The BPS for the fee cannot exceed the `MAX_PLATFORM_FEE_BPS`. + */ + error InvalidPlatformFeeBPS(); + + /** + * @dev The BPS for the fee cannot exceed the `MAX_ARTIST_FEE_BPS`. + */ + error InvalidArtistFeeBPS(); + + /** + * @dev The BPS for the fee cannot exceed the `MAX_AFFILAITE_FEE_BPS`. + */ + error InvalidAffiliateFeeBPS(); + + /** + * @dev The BPS for the fee cannot exceed the `MAX_GOLDEN_EGG_FEE_BPS`. + */ + error InvalidGoldenEggFeeBPS(); + + /** + * @dev The platform per-transaction flat fee cannot exceed the `MAX_PLATFORM_PER_TX_FLAT_FEE`. + */ + error InvalidPlatformPerTxFlatFee(); + + /** + * @dev The `affiliate` provided is invalid for the given `affiliateProof`. + */ + error InvalidAffiliate(); + + /** + * @dev Cannot buy. + */ + error BuyIsFrozen(); + + /** + * @dev The purchase cannot exceed the max supply. + * @param available The number of tokens remaining available for mint. + */ + error ExceedsMaxSupply(uint32 available); + + /** + * @dev The platform fee address cannot be zero. + */ + error PlatformFeeAddressIsZero(); + + /** + * @dev There already is a Sound Automated Market for `edition`. + */ + error SAMAlreadyExists(); + + /** + * @dev There is no Sound Automated Market for `edition`. + */ + error SAMDoesNotExist(); + + /** + * @dev Cannot mint zero tokens. + */ + error MintZeroQuantity(); + + /** + * @dev Cannot burn zero tokens. + */ + error BurnZeroQuantity(); + + /** + * @dev The bytecode hash of the edition is not approved. + */ + error UnapprovedEdition(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Creates a Sound Automated Market on `edition`. + * @param edition The edition address. + * @param basePrice The price added to the bonding curve price. + * @param linearPriceSlope The linear price slope of the bonding curve. + * @param inflectionPrice The sigmoid inflection price of the bonding curve. + * @param inflectionPoint The sigmoid inflection point of the bonding curve. + * @param maxSupply The supply cap for buying tokens. + * @param buyFreezeTime The cutoff time for buying tokens. + * @param artistFeeBPS The fee BPS (basis points) to pay the artist. + * @param goldenEggFeeBPS The fee BPS (basis points) to pay the golden egg holder. + * @param affiliateFeeBPS The fee BPS (basis points) to pay affiliates. + * @param editionBy The address which created the edition via the factory. + * @param editionSalt The salt used to create the edition via the factory. + */ + function create( + address edition, + uint96 basePrice, + uint128 linearPriceSlope, + uint128 inflectionPrice, + uint32 inflectionPoint, + uint32 maxSupply, + uint32 buyFreezeTime, + uint16 artistFeeBPS, + uint16 goldenEggFeeBPS, + uint16 affiliateFeeBPS, + address editionBy, + bytes32 editionSalt + ) external; + + /** + * @dev Mints (buys) tokens for a given edition. + * @param edition The edition address. + * @param to The address to mint to. + * @param quantity Token quantity to mint in song `edition`. + * @param affiliate The affiliate address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @param attributionId The attribution ID. + */ + function buy( + address edition, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof, + uint256 attributionId + ) external payable; + + /** + * @dev Burns (sell) tokens for a given edition. + * @param edition The edition address. + * @param tokenIds The token IDs to burn. + * @param minimumPayout The minimum payout for the transaction to succeed. + * @param payoutTo The address to send the payout to. + * @param attributionId The attribution ID. + */ + function sell( + address edition, + uint256[] calldata tokenIds, + uint256 minimumPayout, + address payoutTo, + uint256 attributionId + ) external; + + /** + * @dev Sets the base price for `edition`. + * This will be added to the bonding curve price. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param basePrice The price added to the bonding curve price. + */ + function setBasePrice(address edition, uint96 basePrice) external; + + /** + * @dev Sets the linear price slope for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param linearPriceSlope The linear price slope of the bonding curve. + */ + function setLinearPriceSlope(address edition, uint128 linearPriceSlope) external; + + /** + * @dev Sets the bonding curve inflection price for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param inflectionPrice The sigmoid inflection price of the bonding curve. + */ + function setInflectionPrice(address edition, uint128 inflectionPrice) external; + + /** + * @dev Sets the bonding curve inflection point for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param inflectionPoint The sigmoid inflection point of the bonding curve. + */ + function setInflectionPoint(address edition, uint32 inflectionPoint) external; + + /** + * @dev Sets the artist fee for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param bps The artist fee in basis points. + */ + function setArtistFee(address edition, uint16 bps) external; + + /** + * @dev Sets the affiliate fee for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param bps The affiliate fee in basis points. + */ + function setAffiliateFee(address edition, uint16 bps) external; + + /** + * @dev Sets the affiliate Merkle root for (`edition`, `mintId`). + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param root The affiliate Merkle root, if any. + */ + function setAffiliateMerkleRoot(address edition, bytes32 root) external; + + /** + * @dev Sets the golden egg fee for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + * @param bps The golden egg fee in basis points. + */ + function setGoldenEggFee(address edition, uint16 bps) external; + + /** + * @dev Sets the supply cap for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + */ + function setMaxSupply(address edition, uint32 maxSupply) external; + + /** + * @dev Sets the buy freeze time for `edition`. + * + * Calling conditions: + * - The caller must be the edition's owner or admin. + * + * @param edition The edition address. + */ + function setBuyFreezeTime(address edition, uint32 buyFreezeTime) external; + + /** + * @dev Withdraws all the accrued fees for `affiliate`. + * @param affiliate The affiliate address. + */ + function withdrawForAffiliate(address affiliate) external; + + /** + * @dev Withdraws all the accrued fees for the platform. + */ + function withdrawForPlatform() external; + + /** + * @dev Withdraws all the accrued fees for the golden egg. + * @param edition The edition address. + */ + function withdrawForGoldenEgg(address edition) external; + + /** + * @dev Sets the platform fee bps. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param bps The platform fee in basis points. + */ + function setPlatformFee(uint16 bps) external; + + /** + * @dev Sets the platform per-transaction flat fee. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param perTxFlatFee The platform per-transaction flat fee. + */ + function setPlatformPerTxFlatFee(uint96 perTxFlatFee) external; + + /** + * @dev Sets the platform fee address. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param addr The platform fee address. + */ + function setPlatformFeeAddress(address addr) external; + + /** + * @dev Sets the list of approved edition factories. + * + * Calling conditions: + * - The caller must be the owner of the contract. + * + * @param factories The list of approved edition factories. + */ + function setApprovedEditionFactories(address[] calldata factories) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev This is the denominator, in basis points (BPS), for any of the fees. + * @return The constant value. + */ + function BPS_DENOMINATOR() external pure returns (uint16); + + /** + * @dev The maximum basis points (BPS) limit allowed for the platform fees. + * @return The constant value. + */ + function MAX_PLATFORM_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum basis points (BPS) limit allowed for the artist fees. + * @return The constant value. + */ + function MAX_ARTIST_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum basis points (BPS) limit allowed for the affiliate fees. + * @return The constant value. + */ + function MAX_AFFILIATE_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum basis points (BPS) limit allowed for the golden egg fees. + * @return The constant value. + */ + function MAX_GOLDEN_EGG_FEE_BPS() external pure returns (uint16); + + /** + * @dev The maximum limit allowed for platform per-transaction flat fee. + * @return The constant value. + */ + function MAX_PLATFORM_PER_TX_FLAT_FEE() external pure returns (uint96); + + /** + * @dev Returns the platform fee basis points. + * @return The configured value. + */ + function platformFeeBPS() external returns (uint16); + + /** + * @dev Returns the platform per-transaction flat fee. + * @return The configured value. + */ + function platformPerTxFlatFee() external returns (uint96); + + /** + * @dev Returns the platform fee address. + * @return The configured value. + */ + function platformFeeAddress() external returns (address); + + /** + * @dev Returns the information for the Sound Automated Market for `edition`. + * @param edition The edition address. + * @return The latest value. + */ + function samInfo(address edition) external view returns (SAMInfo memory); + + /** + * @dev Returns the total value under the bonding curve for `quantity`, from `fromSupply`. + * @param edition The edition address. + * @param fromSupply The starting number of tokens in the bonding curve. + * @param quantity The number of tokens. + * @return The computed value. + */ + function totalValue( + address edition, + uint32 fromSupply, + uint32 quantity + ) external view returns (uint256); + + /** + * @dev Returns the total amount of ETH required to buy from + * `supply + supplyForwardOffset` to `supply + supplyForwardOffset + quantity`. + * @param edition The edition address. + * @param supplyForwardOffset The offset added to the current supply. + * @param quantity The number of tokens. + * @return total The total amount required to be paid, inclusive of all the buy fees. + * @return platformFee The platform fee (inclusive of any flat fees). + * @return artistFee The artist fee. + * @return goldenEggFee The golden egg fee. + * @return affiliateFee The affiliate fee. + */ + function totalBuyPriceAndFees( + address edition, + uint32 supplyForwardOffset, + uint32 quantity + ) + external + view + returns ( + uint256 total, + uint256 platformFee, + uint256 artistFee, + uint256 goldenEggFee, + uint256 affiliateFee + ); + + /** + * @dev Returns the total amount of ETH required to sell from + * `supply - supplyBackwardOffset` to `supply - supplyBackwardOffset - quantity`. + * @param edition The edition address. + * @param supplyBackwardOffset The offset added to the current supply. + * @param quantity The number of tokens. + * @return The computed value. + */ + function totalSellPrice( + address edition, + uint32 supplyBackwardOffset, + uint32 quantity + ) external view returns (uint256); + + /** + * @dev The total fees accrued for the golden egg on `edition`. + * @param edition The edition address. + * @return The latest value. + */ + function goldenEggFeesAccrued(address edition) external view returns (uint128); + + /** + * @dev The receipient of the golden egg fees on `edition`. + * If there is no golden egg winner, the `receipient` will be the `edition`. + * @param edition The edition address. + * @return receipient The latest value. + */ + function goldenEggFeeRecipient(address edition) external view returns (address receipient); + + /** + * @dev The total fees accrued for `affiliate`. + * @param affiliate The affiliate's address. + * @return The latest value. + */ + function affiliateFeesAccrued(address affiliate) external view returns (uint128); + + /** + * @dev The total fees accrued for the platform. + * @return The latest value. + */ + function platformFeesAccrued() external view returns (uint128); + + /** + * @dev Whether `affiliate` is affiliated for `edition`. + * @param edition The edition's address. + * @param affiliate The affiliate's address. + * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any. + * @return The computed value. + */ + function isAffiliatedWithProof( + address edition, + address affiliate, + bytes32[] calldata affiliateProof + ) external view returns (bool); + + /** + * @dev Whether `affiliate` is affiliated for `edition`. + * @param edition The edition's address. + * @param affiliate The affiliate's address. + * @return The computed value. + */ + function isAffiliated(address edition, address affiliate) external view returns (bool); + + /** + * @dev Returns the list of approved edition factories. + * @return The latest values. + */ + function approvedEditionFactories() external view returns (address[] memory); + + /** + * @dev Returns the affiliate Merkle root. + * @param edition The edition's address. + * @return The latest value. + */ + function affiliateMerkleRoot(address edition) external view returns (bytes32); + + /** + * @dev Returns the module's interface ID. + * @return The constant value. + */ + function moduleInterfaceId() external pure returns (bytes4); +} diff --git a/package.json b/package.json index 26846b66..13000fa9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "prettier:write": "prettier --write **/*.{js,ts,json,sol}", "postpublish": "gh-release", "release": "pnpm publish --access=public --no-git-checks", - "release:canary": "(node script/js/canary-release.js && pnpm -r publish --access public --no-git-checks --tag alpha) || echo Skipping Canary...", + "release:canary": "(node script/js/canary-release.js && pnpm publish --access public --no-git-checks --tag alpha) || echo Skipping Canary...", "slither": "slither . --config-file slither.config.json", "solhint": "solhint -f table contracts/**/*.sol", "test": "forge test", diff --git a/script/solidity/Deploy.1.2.0.s.sol b/script/solidity/Deploy.1.2.0.s.sol index 5fcf9ac1..aa785568 100644 --- a/script/solidity/Deploy.1.2.0.s.sol +++ b/script/solidity/Deploy.1.2.0.s.sol @@ -9,10 +9,10 @@ import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { IMetadataModule } from "@core/interfaces/IMetadataModule.sol"; import { GoldenEggMetadata } from "@modules/GoldenEggMetadata.sol"; -import { FixedPriceSignatureMinterV2 } from "@modules/FixedPriceSignatureMinterV2.sol"; -import { MerkleDropMinterV2 } from "@modules/MerkleDropMinterV2.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; -import { EditionMaxMinterV2 } from "@modules/EditionMaxMinterV2.sol"; +import { FixedPriceSignatureMinterV2_1 } from "@modules/FixedPriceSignatureMinterV2_1.sol"; +import { MerkleDropMinterV2_1 } from "@modules/MerkleDropMinterV2_1.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; +import { EditionMaxMinterV2_1 } from "@modules/EditionMaxMinterV2_1.sol"; contract Deploy is Script { bool private ONLY_MINTERS = vm.envBool("ONLY_MINTERS"); @@ -23,10 +23,10 @@ contract Deploy is Script { vm.startBroadcast(); // Deploy minter modules - new FixedPriceSignatureMinterV2(); - new MerkleDropMinterV2(); - new RangeEditionMinterV2(); - new EditionMaxMinterV2(); + new FixedPriceSignatureMinterV2_1(); + new MerkleDropMinterV2_1(); + new RangeEditionMinterV2_1(); + new EditionMaxMinterV2_1(); // If only deploying minters, we're done. if (ONLY_MINTERS) return; diff --git a/script/solidity/GetInterfaceId.s.sol b/script/solidity/GetInterfaceId.s.sol index 2ff9f208..72e6ed1c 100644 --- a/script/solidity/GetInterfaceId.s.sol +++ b/script/solidity/GetInterfaceId.s.sol @@ -11,16 +11,29 @@ import { IMerkleDropMinter } from "@modules/interfaces/IMerkleDropMinter.sol"; import { IEditionMaxMinter } from "@modules/interfaces/IEditionMaxMinter.sol"; import { IRangeEditionMinter } from "@modules/interfaces/IRangeEditionMinter.sol"; + import { IMinterModuleV2 } from "@core/interfaces/IMinterModuleV2.sol"; import { IFixedPriceSignatureMinterV2 } from "@modules/interfaces/IFixedPriceSignatureMinterV2.sol"; import { IMerkleDropMinterV2 } from "@modules/interfaces/IMerkleDropMinterV2.sol"; import { IEditionMaxMinterV2 } from "@modules/interfaces/IEditionMaxMinterV2.sol"; import { IRangeEditionMinterV2 } from "@modules/interfaces/IRangeEditionMinterV2.sol"; + +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { IFixedPriceSignatureMinterV2_1 } from "@modules/interfaces/IFixedPriceSignatureMinterV2_1.sol"; +import { IMerkleDropMinterV2_1 } from "@modules/interfaces/IMerkleDropMinterV2_1.sol"; +import { IEditionMaxMinterV2_1 } from "@modules/interfaces/IEditionMaxMinterV2_1.sol"; +import { IRangeEditionMinterV2_1 } from "@modules/interfaces/IRangeEditionMinterV2_1.sol"; + +import { ISAM } from "@modules/interfaces/ISAM.sol"; +import { ISAMV1_1 } from "@modules/interfaces/ISAMV1_1.sol"; + contract GetInterfaceId is Script { function run() external view { console.log("{"); + // Core. + /* solhint-disable quotes */ console.log('"ISoundEditionV1": "'); console.logBytes4(type(ISoundEditionV1).interfaceId); @@ -28,6 +41,8 @@ contract GetInterfaceId is Script { console.log('", "ISoundEditionV1_2": "'); console.logBytes4(type(ISoundEditionV1_2).interfaceId); + // Modules. + console.log('", "IMinterModule": "'); console.logBytes4(type(IMinterModule).interfaceId); @@ -41,7 +56,7 @@ contract GetInterfaceId is Script { console.logBytes4(type(IEditionMaxMinter).interfaceId); console.log('", "IRangeEditionMinter": "'); - console.logBytes4(type(IRangeEditionMinter).interfaceId); + console.logBytes4(type(IRangeEditionMinter).interfaceId); // v2 console.log('", "IMinterModuleV2": "'); @@ -59,6 +74,28 @@ contract GetInterfaceId is Script { console.log('", "IRangeEditionMinterV2": "'); console.logBytes4(type(IRangeEditionMinterV2).interfaceId); + // v2_1 + console.log('", "IMinterModuleV2_1": "'); + console.logBytes4(type(IMinterModuleV2_1).interfaceId); + + console.log('", "IFixedPriceSignatureMinterV2_1": "'); + console.logBytes4(type(IFixedPriceSignatureMinterV2_1).interfaceId); + + console.log('", "IMerkleDropMinterV2_1": "'); + console.logBytes4(type(IMerkleDropMinterV2_1).interfaceId); + + console.log('", "IEditionMaxMinterV2_1": "'); + console.logBytes4(type(IEditionMaxMinterV2_1).interfaceId); + + console.log('", "IRangeEditionMinterV2_1": "'); + console.logBytes4(type(IRangeEditionMinterV2_1).interfaceId); + + console.log('", "ISAM": "'); + console.logBytes4(type(ISAM).interfaceId); + + console.log('", "ISAMV1_1": "'); + console.logBytes4(type(ISAMV1_1).interfaceId); + console.log('"}'); } } diff --git a/src/interfaceIds.ts b/src/interfaceIds.ts index bc2e3ffc..85721e72 100644 --- a/src/interfaceIds.ts +++ b/src/interfaceIds.ts @@ -11,4 +11,11 @@ export const interfaceIds = { IMerkleDropMinterV2: "0x5e9a2e5f", IEditionMaxMinterV2: "0x6ee3f258", IRangeEditionMinterV2: "0x84435ae5", + IMinterModuleV2_1: "0x7a2b8b9b", + IFixedPriceSignatureMinterV2_1: "0x0f713f15", + IMerkleDropMinterV2_1: "0x6328e9ad", + IEditionMaxMinterV2_1: "0x6ee3f258", + IRangeEditionMinterV2_1: "0xb9f19d17", + ISAM: "0xa3c2dbc7", + ISAMV1_1: "0x212580d2", } as const; diff --git a/src/json/interfaceIds.json b/src/json/interfaceIds.json index 97c5f4eb..2cb1f00d 100644 --- a/src/json/interfaceIds.json +++ b/src/json/interfaceIds.json @@ -1,14 +1 @@ -{ - "ISoundEditionV1": "0x50899e54", - "ISoundEditionV1_2": "0xa176eca6", - "IMinterModule": "0x37c74bd8", - "IFixedPriceSignatureMinter": "0xa61bd96f", - "IMerkleDropMinter": "0x89691c4c", - "IEditionMaxMinter": "0xa7ea8688", - "IRangeEditionMinter": "0x4d4a2e35", - "IMinterModuleV2": "0xf8ccd08e", - "IFixedPriceSignatureMinterV2": "0x0f713f15", - "IMerkleDropMinterV2": "0x5e9a2e5f", - "IEditionMaxMinterV2": "0x6ee3f258", - "IRangeEditionMinterV2": "0x84435ae5" -} +{"ISoundEditionV1":"0x50899e54","ISoundEditionV1_2":"0xa176eca6","IMinterModule":"0x37c74bd8","IFixedPriceSignatureMinter":"0xa61bd96f","IMerkleDropMinter":"0x89691c4c","IEditionMaxMinter":"0xa7ea8688","IRangeEditionMinter":"0x4d4a2e35","IMinterModuleV2":"0xf8ccd08e","IFixedPriceSignatureMinterV2":"0x0f713f15","IMerkleDropMinterV2":"0x5e9a2e5f","IEditionMaxMinterV2":"0x6ee3f258","IRangeEditionMinterV2":"0x84435ae5","IMinterModuleV2_1":"0x7a2b8b9b","IFixedPriceSignatureMinterV2_1":"0x0f713f15","IMerkleDropMinterV2_1":"0x6328e9ad","IEditionMaxMinterV2_1":"0x6ee3f258","IRangeEditionMinterV2_1":"0xb9f19d17","ISAM":"0xa3c2dbc7","ISAMV1_1":"0x212580d2"} \ No newline at end of file diff --git a/tests/core/SoundCreator.t.sol b/tests/core/SoundCreator.t.sol index dbbf9fbd..2864613c 100644 --- a/tests/core/SoundCreator.t.sol +++ b/tests/core/SoundCreator.t.sol @@ -5,9 +5,9 @@ import { ISoundCreatorV1 } from "@core/interfaces/ISoundCreatorV1.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol"; -import { FixedPriceSignatureMinterV2 } from "@modules/FixedPriceSignatureMinterV2.sol"; -import { MerkleDropMinterV2 } from "@modules/MerkleDropMinterV2.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; +import { FixedPriceSignatureMinterV2_1 } from "@modules/FixedPriceSignatureMinterV2_1.sol"; +import { MerkleDropMinterV2_1 } from "@modules/MerkleDropMinterV2_1.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../TestConfig.sol"; @@ -158,15 +158,15 @@ contract SoundCreatorTests is TestConfig { address[] memory contracts = new address[](6); bytes[] memory data = new bytes[](6); - FixedPriceSignatureMinterV2 signatureMinter; - MerkleDropMinterV2 merkleMinter; - RangeEditionMinterV2 rangeMinter; + FixedPriceSignatureMinterV2_1 signatureMinter; + MerkleDropMinterV2_1 merkleMinter; + RangeEditionMinterV2_1 rangeMinter; // Deploy minters. { - signatureMinter = new FixedPriceSignatureMinterV2(); - merkleMinter = new MerkleDropMinterV2(); - rangeMinter = new RangeEditionMinterV2(); + signatureMinter = new FixedPriceSignatureMinterV2_1(); + merkleMinter = new MerkleDropMinterV2_1(); + rangeMinter = new RangeEditionMinterV2_1(); } // Deploy the implementation of the edition. diff --git a/tests/invariants/RangeEditionMinterV2Invariants.sol b/tests/invariants/RangeEditionMinterV2_1Invariants.sol similarity index 73% rename from tests/invariants/RangeEditionMinterV2Invariants.sol rename to tests/invariants/RangeEditionMinterV2_1Invariants.sol index 7a9ad582..6f9fa8e0 100644 --- a/tests/invariants/RangeEditionMinterV2Invariants.sol +++ b/tests/invariants/RangeEditionMinterV2_1Invariants.sol @@ -2,15 +2,15 @@ pragma solidity ^0.8.16; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; -import { MintInfo } from "@modules/interfaces/IRangeEditionMinterV2.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; -import { BaseMinterV2 } from "@modules/BaseMinterV2.sol"; -import { RangeEditionMinterV2Tests } from "../modules/RangeEditionMinterV2.t.sol"; +import { MintInfo } from "@modules/interfaces/IRangeEditionMinterV2_1.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; +import { BaseMinterV2_1 } from "@modules/BaseMinterV2_1.sol"; +import { RangeEditionMinterV2_1Tests } from "../modules/RangeEditionMinterV2_1.t.sol"; import { InvariantTest } from "./InvariantTest.sol"; -contract RangeEditionMinterV2Invariants is RangeEditionMinterV2Tests, InvariantTest { - RangeEditionMinterV2Updater minterUpdater; - RangeEditionMinterV2 minter; +contract RangeEditionMinterV2_1Invariants is RangeEditionMinterV2_1Tests, InvariantTest { + RangeEditionMinterV2_1Updater minterUpdater; + RangeEditionMinterV2_1 minter; SoundEditionV1_2 edition; function setUp() public override { @@ -18,7 +18,7 @@ contract RangeEditionMinterV2Invariants is RangeEditionMinterV2Tests, InvariantT edition = createGenericEdition(); - minter = new RangeEditionMinterV2(); + minter = new RangeEditionMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -34,7 +34,7 @@ contract RangeEditionMinterV2Invariants is RangeEditionMinterV2Tests, InvariantT MAX_MINTABLE_PER_ACCOUNT ); - minterUpdater = new RangeEditionMinterV2Updater(edition, minter); + minterUpdater = new RangeEditionMinterV2_1Updater(edition, minter); addTargetContract(address(minter)); } @@ -54,13 +54,13 @@ contract RangeEditionMinterV2Invariants is RangeEditionMinterV2Tests, InvariantT } } -contract RangeEditionMinterV2Updater { +contract RangeEditionMinterV2_1Updater { uint128 constant MINT_ID = 0; SoundEditionV1_2 edition; - RangeEditionMinterV2 minter; + RangeEditionMinterV2_1 minter; - constructor(SoundEditionV1_2 _edition, RangeEditionMinterV2 _minter) { + constructor(SoundEditionV1_2 _edition, RangeEditionMinterV2_1 _minter) { edition = _edition; minter = _minter; } diff --git a/tests/mocks/MockMinterV2.sol b/tests/mocks/MockMinterV2_1.sol similarity index 75% rename from tests/mocks/MockMinterV2.sol rename to tests/mocks/MockMinterV2_1.sol index 6f2e76c3..f2943999 100644 --- a/tests/mocks/MockMinterV2.sol +++ b/tests/mocks/MockMinterV2_1.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.16; import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol"; -import { BaseMinterV2 } from "@modules/BaseMinterV2.sol"; +import { BaseMinterV2_1 } from "@modules/BaseMinterV2_1.sol"; struct MintInfo { uint32 startTime; @@ -12,7 +12,7 @@ struct MintInfo { bool mintPaused; } -contract MockMinterV2 is BaseMinterV2 { +contract MockMinterV2_1 is BaseMinterV2_1 { uint96 private _currentPrice; function createEditionMint( @@ -45,20 +45,21 @@ contract MockMinterV2 is BaseMinterV2 { mintTo(edition, mintId, msg.sender, quantity, affiliate, MerkleProofLib.emptyProof(), 0); } - function setPrice(uint96 price) external { - _currentPrice = price; + function setPrice( + address edition, + uint128 mintId, + uint96 price + ) external { + _getBaseData(edition, mintId).price = price; } - function totalPrice( - address, /* edition */ - uint128, /* mintId */ - address, /* to */ + function requiredEtherValue( + address edition, + uint128 mintId, uint32 quantity - ) public view virtual override(BaseMinterV2) returns (uint128) { - unchecked { - // Will not overflow, as `price` is 96 bits, and `quantity` is 32 bits. 96 + 32 = 128. - return uint128(uint256(_currentPrice) * uint256(quantity)); - } + ) external view returns (uint256) { + (uint256 total, , , , ) = totalPriceAndFees(edition, mintId, quantity); + return total; } function mintInfo(address edition, uint128 mintId) external view returns (MintInfo memory) { diff --git a/tests/modules/BaseMinterV2.t.sol b/tests/modules/BaseMinterV2_1.t.sol similarity index 82% rename from tests/modules/BaseMinterV2.t.sol rename to tests/modules/BaseMinterV2_1.t.sol index 065e92de..5534c2d4 100644 --- a/tests/modules/BaseMinterV2.t.sol +++ b/tests/modules/BaseMinterV2_1.t.sol @@ -5,9 +5,10 @@ import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgr import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { TestConfig } from "../TestConfig.sol"; -import { MockMinterV2, MintInfo } from "../mocks/MockMinterV2.sol"; +import { MockMinterV2_1, MintInfo } from "../mocks/MockMinterV2_1.sol"; import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { IMinterModuleV2 } from "@core/interfaces/IMinterModuleV2.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { Ownable } from "solady/auth/Ownable.sol"; @@ -33,6 +34,8 @@ contract MintControllerBaseV2Tests is TestConfig { event PlatformFlatFeeSet(uint96 flatFee); + event PlatformPerTxFlatFeeSet(uint96 perTxFlatFee); + event PlatformFeeAddressSet(address addr); event Minted( @@ -49,7 +52,7 @@ contract MintControllerBaseV2Tests is TestConfig { uint256 indexed attributionId ); - MockMinterV2 public minter; + MockMinterV2_1 public minter; uint32 constant START_TIME = 0; uint32 constant END_TIME = type(uint32).max; @@ -58,7 +61,7 @@ contract MintControllerBaseV2Tests is TestConfig { function setUp() public override { super.setUp(); - minter = new MockMinterV2(); + minter = new MockMinterV2_1(); } function _createEdition(uint32 editionMaxMintable) internal returns (SoundEditionV1_2 edition) { @@ -94,7 +97,7 @@ contract MintControllerBaseV2Tests is TestConfig { SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint16 affiliateFeeBPS = minter.MAX_AFFILIATE_FEE_BPS() + 1; - vm.expectRevert(IMinterModuleV2.InvalidAffiliateFeeBPS.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidAffiliateFeeBPS.selector); minter.createEditionMint(address(edition), START_TIME, END_TIME, affiliateFeeBPS); } @@ -146,12 +149,12 @@ contract MintControllerBaseV2Tests is TestConfig { uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint96 price = 1; - minter.setPrice(price); + minter.setPrice(address(edition), mintId, price); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, price * 2 - 1, price * 2)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, price * 2 - 1, price * 2)); minter.mint{ value: price * 2 - 1 }(address(edition), mintId, 2, address(0)); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, price * 2 + 1, price * 2)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, price * 2 + 1, price * 2)); minter.mint{ value: price * 2 + 1 }(address(edition), mintId, 2, address(0)); minter.mint{ value: price * 2 }(address(edition), mintId, 2, address(0)); @@ -163,7 +166,7 @@ contract MintControllerBaseV2Tests is TestConfig { uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint96 price = 1; - minter.setPrice(price); + minter.setPrice(address(edition), mintId, price); uint32 quantity = 2; @@ -187,9 +190,9 @@ contract MintControllerBaseV2Tests is TestConfig { minter.setEditionMintPaused(address(edition), mintId, true); uint96 price = 1; - minter.setPrice(price); + minter.setPrice(address(edition), mintId, price); - vm.expectRevert(IMinterModuleV2.MintPaused.selector); + vm.expectRevert(IMinterModuleV2_1.MintPaused.selector); minter.mint{ value: price * 2 }(address(edition), mintId, 2, address(0)); @@ -199,8 +202,6 @@ contract MintControllerBaseV2Tests is TestConfig { } function test_mintRevertsWithZeroQuantity() public { - minter.setPrice(0); - SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -220,8 +221,6 @@ contract MintControllerBaseV2Tests is TestConfig { } function test_cantMintPastEditionMaxMintable() external { - minter.setPrice(0); - uint32 maxSupply = 50; SoundEditionV1_2 edition1 = _createEdition(maxSupply); @@ -298,10 +297,10 @@ contract MintControllerBaseV2Tests is TestConfig { minter.setAffiliateMerkleRoot(address(edition), mintId, m.getRoot(leaves)); uint32 quantity = 1; - uint256 requiredEtherValue = minter.totalPrice(address(edition), mintId, address(this), quantity); + uint256 requiredEtherValue = minter.requiredEtherValue(address(edition), mintId, quantity); bytes32[] memory affiliateProof = m.getProof(leaves, 1); - vm.expectRevert(IMinterModuleV2.InvalidAffiliate.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidAffiliate.selector); minter.mintTo{ value: requiredEtherValue }( address(edition), mintId, @@ -329,7 +328,7 @@ contract MintControllerBaseV2Tests is TestConfig { uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint96 price = 1 ether; - minter.setPrice(price); + minter.setPrice(address(edition), mintId, price); minter.setPlatformFee(0); minter.setPlatformFeeAddress(SOUND_FEE_ADDRESS); @@ -337,7 +336,7 @@ contract MintControllerBaseV2Tests is TestConfig { _test_setAffiliateFee(edition, mintId, affiliateFeeBPS); uint32 quantity = 1; - uint256 requiredEtherValue = minter.totalPrice(address(edition), mintId, address(this), quantity); + uint256 requiredEtherValue = minter.requiredEtherValue(address(edition), mintId, quantity); address affiliate = getFundedAccount(123456789); @@ -358,14 +357,14 @@ contract MintControllerBaseV2Tests is TestConfig { uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint96 price = 1 ether; - minter.setPrice(price); + minter.setPrice(address(edition), mintId, price); uint16 platformFeeBPS = uint16(_bound(_random(), 0, minter.MAX_PLATFORM_FEE_BPS())); minter.setPlatformFee(platformFeeBPS); minter.setPlatformFeeAddress(SOUND_FEE_ADDRESS); uint32 quantity = 1; - uint256 requiredEtherValue = minter.totalPrice(address(edition), mintId, address(this), quantity); + uint256 requiredEtherValue = minter.requiredEtherValue(address(edition), mintId, quantity); address affiliate = getFundedAccount(123456789); @@ -376,8 +375,53 @@ contract MintControllerBaseV2Tests is TestConfig { _test_withdrawPlatformFeesAccrued(expectedPlatformFees); } + function testTotalPriceAndFeesNoOverflow( + uint96 price, + uint32 quantity, + uint16 platformFlatFee, + uint16 platformFeeBPS, + uint96 platformPerTxFlatFee, + uint16 BPS_DENOMINATOR, + uint16 affiliateFeeBPS + ) public { + if (BPS_DENOMINATOR == 0) BPS_DENOMINATOR = 1; + + uint256 subTotal = uint256(quantity) * uint256(price); + + uint256 platformFlatFeeTotal = uint256(quantity) * uint256(platformFlatFee) + uint256(platformPerTxFlatFee); + + uint256 total = subTotal + platformFlatFeeTotal; + + uint256 platformFee = (subTotal * uint256(platformFeeBPS)) / uint256(BPS_DENOMINATOR) + platformFlatFeeTotal; + + uint256 affiliateFee = (subTotal * uint256(affiliateFeeBPS)) / uint256(BPS_DENOMINATOR); + + assertTrue(subTotal + platformFlatFeeTotal + total + platformFee + affiliateFee < type(uint256).max); + } + + function testTotalPriceAndFeesNoOverflow() public { + testTotalPriceAndFeesNoOverflow( + type(uint96).max, + type(uint32).max, + type(uint16).max, + type(uint16).max, + type(uint96).max, + type(uint16).max, + type(uint16).max + ); + testTotalPriceAndFeesNoOverflow( + type(uint96).max, + type(uint32).max, + type(uint16).max, + type(uint16).max, + type(uint96).max, + 0, + type(uint16).max + ); + } + struct _TestTemps { - uint256 totalPrice; + uint256 subTotal; uint256 requiredEtherValue; bool affiliated; address affiliate; @@ -388,6 +432,7 @@ contract MintControllerBaseV2Tests is TestConfig { uint256 expectedAffiliateFees; uint256 platformFeeBPS; uint256 platformFlatFee; + uint256 platformPerTxFlatFee; uint256 affiliateFeeBPS; } @@ -400,8 +445,6 @@ contract MintControllerBaseV2Tests is TestConfig { t.price = _bound(_random(), 0, type(uint96).max); t.quantity = _bound(_random(), 1, 8); - minter.setPrice(uint96(t.price)); - SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); (t.affiliate, ) = _randomSigner(); @@ -413,6 +456,8 @@ contract MintControllerBaseV2Tests is TestConfig { uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); + minter.setPrice(address(edition), mintId, uint96(t.price)); + t.platformFeeBPS = _bound(_random(), 0, minter.MAX_PLATFORM_FEE_BPS()); vm.expectEmit(true, true, true, true); emit PlatformFeeSet(uint16(t.platformFeeBPS)); @@ -426,22 +471,32 @@ contract MintControllerBaseV2Tests is TestConfig { emit PlatformFlatFeeSet(uint96(t.platformFlatFee)); minter.setPlatformFlatFee(uint96(t.platformFlatFee)); } + if (_random() % 2 == 0) { + t.platformPerTxFlatFee = _bound(_random(), 1, minter.MAX_PLATFORM_PER_TX_FLAT_FEE()); + vm.expectEmit(true, true, true, true); + emit PlatformPerTxFlatFeeSet(uint96(t.platformPerTxFlatFee)); + minter.setPlatformPerTxFlatFee(uint96(t.platformPerTxFlatFee)); + } t.affiliateFeeBPS = _bound(_random(), 0, minter.MAX_AFFILIATE_FEE_BPS() * 2); if (!_test_setAffiliateFee(edition, mintId, uint16(t.affiliateFeeBPS))) return; - t.totalPrice = minter.totalPrice(address(edition), mintId, address(this), uint32(t.quantity)); - t.requiredEtherValue = t.totalPrice; + (, t.subTotal, , , ) = minter.totalPriceAndFees(address(edition), mintId, uint32(t.quantity)); + t.requiredEtherValue = t.subTotal; - t.expectedPlatformFees = (t.totalPrice * t.platformFeeBPS) / minter.BPS_DENOMINATOR(); + t.expectedPlatformFees = (t.subTotal * t.platformFeeBPS) / minter.BPS_DENOMINATOR(); if (t.platformFlatFee != 0) { t.expectedPlatformFees += t.platformFlatFee * t.quantity; t.requiredEtherValue += t.platformFlatFee * t.quantity; } + if (t.platformPerTxFlatFee != 0) { + t.expectedPlatformFees += t.platformPerTxFlatFee; + t.requiredEtherValue += t.platformPerTxFlatFee; + } t.affiliated = minter.isAffiliated(address(edition), mintId, t.affiliate); if (t.affiliated) { - t.expectedAffiliateFees = (t.totalPrice * t.affiliateFeeBPS) / minter.BPS_DENOMINATOR(); + t.expectedAffiliateFees = (t.subTotal * t.affiliateFeeBPS) / minter.BPS_DENOMINATOR(); } // Expect an event. uint32 fromTokenId = uint32(edition.nextTokenId()); @@ -508,7 +563,7 @@ contract MintControllerBaseV2Tests is TestConfig { uint16 affiliateFeeBPS ) internal returns (bool) { if (affiliateFeeBPS > minter.MAX_AFFILIATE_FEE_BPS()) { - vm.expectRevert(IMinterModuleV2.InvalidAffiliateFeeBPS.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidAffiliateFeeBPS.selector); minter.setAffiliateFee(address(edition), mintId, affiliateFeeBPS); return false; } @@ -521,6 +576,7 @@ contract MintControllerBaseV2Tests is TestConfig { function test_supportsInterface() external { assertTrue(minter.supportsInterface(type(IMinterModuleV2).interfaceId)); + assertTrue(minter.supportsInterface(type(IMinterModuleV2_1).interfaceId)); assertTrue(minter.supportsInterface(type(IERC165).interfaceId)); assertFalse(minter.supportsInterface(bytes4(0))); } diff --git a/tests/modules/EditionMaxMinterV2.t.sol b/tests/modules/EditionMaxMinterV2_1.t.sol similarity index 77% rename from tests/modules/EditionMaxMinterV2.t.sol rename to tests/modules/EditionMaxMinterV2_1.t.sol index 46b6c84f..30c8fc8c 100644 --- a/tests/modules/EditionMaxMinterV2.t.sol +++ b/tests/modules/EditionMaxMinterV2_1.t.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.16; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; -import { EditionMaxMinterV2 } from "@modules/EditionMaxMinterV2.sol"; -import { IEditionMaxMinterV2, MintInfo } from "@modules/interfaces/IEditionMaxMinterV2.sol"; -import { IMinterModuleV2 } from "@core/interfaces/IMinterModuleV2.sol"; -import { BaseMinterV2 } from "@modules/BaseMinterV2.sol"; +import { EditionMaxMinterV2_1 } from "@modules/EditionMaxMinterV2_1.sol"; +import { IEditionMaxMinterV2_1, MintInfo } from "@modules/interfaces/IEditionMaxMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { BaseMinterV2_1 } from "@modules/BaseMinterV2_1.sol"; import { TestConfig } from "../TestConfig.sol"; -contract EditionMaxMinterV2Tests is TestConfig { +contract EditionMaxMinterV2_1Tests is TestConfig { uint96 constant PRICE = 1; uint32 constant START_TIME = 100; @@ -63,14 +63,14 @@ contract EditionMaxMinterV2Tests is TestConfig { function _createEditionAndMinter(uint32 _maxMintablePerAccount) internal - returns (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) + returns (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) { edition = createGenericEdition(); edition.setEditionMaxMintableRange(MAX_MINTABLE_LOWER, MAX_MINTABLE_UPPER); edition.setEditionCutoffTime(CUTOFF_TIME); - minter = new EditionMaxMinterV2(); + minter = new EditionMaxMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -107,18 +107,18 @@ contract EditionMaxMinterV2Tests is TestConfig { ) ); - EditionMaxMinterV2 minter = new EditionMaxMinterV2(); + EditionMaxMinterV2_1 minter = new EditionMaxMinterV2_1(); bool hasRevert; if (maxMintablePerAccount == 0) { - vm.expectRevert(IEditionMaxMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IEditionMaxMinterV2_1.MaxMintablePerAccountIsZero.selector); hasRevert = true; } else if (!(startTime < endTime)) { - vm.expectRevert(IMinterModuleV2.InvalidTimeRange.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidTimeRange.selector); hasRevert = true; } else if (affiliateFeeBPS > minter.MAX_AFFILIATE_FEE_BPS()) { - vm.expectRevert(IMinterModuleV2.InvalidAffiliateFeeBPS.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidAffiliateFeeBPS.selector); hasRevert = true; } @@ -149,7 +149,7 @@ contract EditionMaxMinterV2Tests is TestConfig { function test_createEditionMintEmitsEvent() public { SoundEditionV1_2 edition = createGenericEdition(); - EditionMaxMinterV2 minter = new EditionMaxMinterV2(); + EditionMaxMinterV2_1 minter = new EditionMaxMinterV2_1(); vm.expectEmit(true, true, true, true); @@ -167,17 +167,17 @@ contract EditionMaxMinterV2Tests is TestConfig { } function test_mintWhenOverMaxMintablePerAccountReverts() public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(1); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(1); vm.warp(START_TIME); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(IEditionMaxMinterV2.ExceedsMaxPerAccount.selector); + vm.expectRevert(IEditionMaxMinterV2_1.ExceedsMaxPerAccount.selector); minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0)); } function test_mintWhenOverMaxMintableDueToPreviousMintedReverts() public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(3); vm.warp(START_TIME); address caller = getFundedAccount(1); @@ -190,13 +190,13 @@ contract EditionMaxMinterV2Tests is TestConfig { // attempting to mint 2 more reverts vm.prank(caller); - vm.expectRevert(IEditionMaxMinterV2.ExceedsMaxPerAccount.selector); + vm.expectRevert(IEditionMaxMinterV2_1.ExceedsMaxPerAccount.selector); minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0)); } function test_mintWhenMintablePerAccountIsSetAndSatisfied() public { // Set max allowed per account to 3 - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(3); address caller = getFundedAccount(1); @@ -217,7 +217,7 @@ contract EditionMaxMinterV2Tests is TestConfig { } function test_mintUpdatesValuesAndMintsCorrectly() public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.warp(START_TIME); @@ -237,31 +237,31 @@ contract EditionMaxMinterV2Tests is TestConfig { function test_mintRevertForWrongPayment() public { uint32 quantity = 2; - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(quantity); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(quantity); vm.warp(START_TIME); uint256 requiredPayment = quantity * PRICE; vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, requiredPayment - 1, requiredPayment) + abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, requiredPayment - 1, requiredPayment) ); minter.mint{ value: requiredPayment - 1 }(address(edition), MINT_ID, quantity, address(0)); vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, requiredPayment + 1, requiredPayment) + abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, requiredPayment + 1, requiredPayment) ); minter.mint{ value: requiredPayment + 1 }(address(edition), MINT_ID, quantity, address(0)); } function test_mintRevertsForMintNotOpen() public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); uint32 quantity = 1; vm.warp(START_TIME - 1); vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) + abi.encodeWithSelector(IMinterModuleV2_1.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) ); minter.mint{ value: quantity * PRICE }(address(edition), MINT_ID, quantity, address(0)); @@ -276,13 +276,13 @@ contract EditionMaxMinterV2Tests is TestConfig { vm.warp(END_TIME + 1); vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) + abi.encodeWithSelector(IMinterModuleV2_1.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) ); minter.mint{ value: quantity * PRICE }(address(edition), MINT_ID, quantity, address(0)); } function test_setPrice(uint96 price) public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), MINT_ID, price); @@ -293,7 +293,7 @@ contract EditionMaxMinterV2Tests is TestConfig { function test_setMaxMintablePerAccount(uint32 maxMintablePerAccount) public { vm.assume(maxMintablePerAccount != 0); - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit MaxMintablePerAccountSet(address(edition), MINT_ID, maxMintablePerAccount); @@ -303,35 +303,35 @@ contract EditionMaxMinterV2Tests is TestConfig { } function test_setZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - vm.expectRevert(IEditionMaxMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IEditionMaxMinterV2_1.MaxMintablePerAccountIsZero.selector); minter.setMaxMintablePerAccount(address(edition), MINT_ID, 0); } function test_createWithZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_2 edition, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - vm.expectRevert(IEditionMaxMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IEditionMaxMinterV2_1.MaxMintablePerAccountIsZero.selector); minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, AFFILIATE_FEE_BPS, 0); } function test_supportsInterface() public { - (, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - bool supportsIMinterModuleV2 = minter.supportsInterface(type(IMinterModuleV2).interfaceId); - bool supportsIEditionMaxMinterV2 = minter.supportsInterface(type(IEditionMaxMinterV2).interfaceId); + bool supportsIMinterModuleV2_1 = minter.supportsInterface(type(IMinterModuleV2_1).interfaceId); + bool supportsIEditionMaxMinterV2_1 = minter.supportsInterface(type(IEditionMaxMinterV2_1).interfaceId); bool supports165 = minter.supportsInterface(type(IERC165).interfaceId); assertTrue(supports165); - assertTrue(supportsIEditionMaxMinterV2); - assertTrue(supportsIMinterModuleV2); + assertTrue(supportsIEditionMaxMinterV2_1); + assertTrue(supportsIMinterModuleV2_1); } function test_moduleInterfaceId() public { - (, EditionMaxMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (, EditionMaxMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - assertTrue(type(IEditionMaxMinterV2).interfaceId == minter.moduleInterfaceId()); + assertTrue(type(IEditionMaxMinterV2_1).interfaceId == minter.moduleInterfaceId()); } function test_mintInfo( @@ -347,7 +347,7 @@ contract EditionMaxMinterV2Tests is TestConfig { vm.assume(startTime < endTime); vm.assume(editionMaxMintableLower <= editionMaxMintableUpper); - EditionMaxMinterV2 minter = new EditionMaxMinterV2(); + EditionMaxMinterV2_1 minter = new EditionMaxMinterV2_1(); affiliateFeeBPS = uint16(affiliateFeeBPS % minter.MAX_AFFILIATE_FEE_BPS()); diff --git a/tests/modules/FixedPriceSignatureMinterV2.t.sol b/tests/modules/FixedPriceSignatureMinterV2_1.t.sol similarity index 82% rename from tests/modules/FixedPriceSignatureMinterV2.t.sol rename to tests/modules/FixedPriceSignatureMinterV2_1.t.sol index a9f916bd..1f4446c1 100644 --- a/tests/modules/FixedPriceSignatureMinterV2.t.sol +++ b/tests/modules/FixedPriceSignatureMinterV2_1.t.sol @@ -3,19 +3,19 @@ pragma solidity ^0.8.16; import { ECDSA } from "solady/utils/ECDSA.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; -import { IMinterModuleV2 } from "@core/interfaces/IMinterModuleV2.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; -import { FixedPriceSignatureMinterV2 } from "@modules/FixedPriceSignatureMinterV2.sol"; -import { IFixedPriceSignatureMinterV2, MintInfo } from "@modules/interfaces/IFixedPriceSignatureMinterV2.sol"; +import { FixedPriceSignatureMinterV2_1 } from "@modules/FixedPriceSignatureMinterV2_1.sol"; +import { IFixedPriceSignatureMinterV2_1, MintInfo } from "@modules/interfaces/IFixedPriceSignatureMinterV2_1.sol"; import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../TestConfig.sol"; import { Ownable } from "solady/auth/Ownable.sol"; import "forge-std/console.sol"; -contract FixedPriceSignatureMinterV2Tests is TestConfig { +contract FixedPriceSignatureMinterV2_1Tests is TestConfig { using ECDSA for bytes32; uint96 constant PRICE = 1; @@ -88,10 +88,10 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { bytes32 digest = keccak256( abi.encodePacked( "\x19\x01", - IFixedPriceSignatureMinterV2(minter).DOMAIN_SEPARATOR(), + IFixedPriceSignatureMinterV2_1(minter).DOMAIN_SEPARATOR(), keccak256( abi.encode( - IFixedPriceSignatureMinterV2(minter).MINT_TYPEHASH(), + IFixedPriceSignatureMinterV2_1(minter).MINT_TYPEHASH(), buyer, mintId, claimTicket, @@ -106,10 +106,13 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { return abi.encodePacked(r, s, v); } - function _createEditionAndMinter() internal returns (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) { + function _createEditionAndMinter() + internal + returns (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) + { edition = createGenericEdition(); - minter = new FixedPriceSignatureMinterV2(); + minter = new FixedPriceSignatureMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -127,7 +130,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { function test_createEditionMintEmitsEvent() public { SoundEditionV1_2 edition = createGenericEdition(); - FixedPriceSignatureMinterV2 minter = new FixedPriceSignatureMinterV2(); + FixedPriceSignatureMinterV2_1 minter = new FixedPriceSignatureMinterV2_1(); vm.expectEmit(false, false, false, true); @@ -156,9 +159,9 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { function test_createEditionMintRevertsIfSignerIsZeroAddress() public { SoundEditionV1_2 edition = createGenericEdition(); - FixedPriceSignatureMinterV2 minter = new FixedPriceSignatureMinterV2(); + FixedPriceSignatureMinterV2_1 minter = new FixedPriceSignatureMinterV2_1(); - vm.expectRevert(IFixedPriceSignatureMinterV2.SignerIsZeroAddress.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.SignerIsZeroAddress.selector); minter.createEditionMint( address(edition), @@ -172,7 +175,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintRevertsIfBuyerNotAuthorized() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 claimTicket = 0; address buyer = getFundedAccount(1); @@ -204,7 +207,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { // This mint fails because invalidBuyer isn't in the signed message vm.prank(buyer); - vm.expectRevert(IFixedPriceSignatureMinterV2.InvalidSignature.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.InvalidSignature.selector); minter.mint{ value: PRICE }( address(edition), MINT_ID, @@ -217,7 +220,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintWithWrongPaymentReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = quantity; @@ -226,7 +229,9 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { bytes memory sig = _getSignature(buyer, address(minter), MINT_ID, CLAIM_TICKET_0, signedQuantity, buyer); vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, PRICE * quantity - 1, PRICE * 2)); + vm.expectRevert( + abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, PRICE * quantity - 1, PRICE * 2) + ); minter.mint{ value: PRICE * quantity - 1 }( address(edition), MINT_ID, @@ -238,7 +243,9 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { ); vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, PRICE * quantity + 1, PRICE * 2)); + vm.expectRevert( + abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, PRICE * quantity + 1, PRICE * 2) + ); minter.mint{ value: PRICE * quantity + 1 }( address(edition), MINT_ID, @@ -251,7 +258,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintWhenSoldOutReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 claimTicket = 0; uint32 quantity = MAX_MINTABLE + 1; @@ -261,7 +268,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { bytes memory sig1 = _getSignature(buyer, address(minter), MINT_ID, claimTicket, signedQuantity, buyer); vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.ExceedsAvailableSupply.selector, MAX_MINTABLE)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.ExceedsAvailableSupply.selector, MAX_MINTABLE)); minter.mint{ value: PRICE * (MAX_MINTABLE + 1) }( address(edition), MINT_ID, @@ -291,7 +298,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { bytes memory sig3 = _getSignature(buyer, address(minter), MINT_ID, claimTicket++, MAX_MINTABLE, buyer); vm.prank(buyer); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.ExceedsAvailableSupply.selector, 0)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.ExceedsAvailableSupply.selector, 0)); minter.mint{ value: PRICE }( address(edition), MINT_ID, @@ -304,7 +311,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintWithUnauthorizedMinterReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); address buyer = getFundedAccount(1); uint32 claimTicket = 0; @@ -326,7 +333,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { edition.revokeRoles(address(minter), edition.MINTER_ROLE()); vm.prank(buyer); - vm.expectRevert(IFixedPriceSignatureMinterV2.SignatureAlreadyUsed.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.SignatureAlreadyUsed.selector); minter.mint{ value: PRICE }( address(edition), MINT_ID, @@ -339,7 +346,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintForNonExistentMintIdReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = quantity; @@ -354,7 +361,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { uint128 nonExistentMintId = MINT_ID + 1; vm.prank(buyer); - vm.expectRevert(IMinterModuleV2.MintDoesNotExist.selector); + vm.expectRevert(IMinterModuleV2_1.MintDoesNotExist.selector); minter.mint{ value: PRICE * quantity }( address(edition), nonExistentMintId, @@ -367,7 +374,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintUpdatesValuesAndMintsCorrectly() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = quantity; @@ -398,7 +405,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_multipleMintsFromSameBuyer() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 quantity = 1; uint32 signedQuantity = 2; @@ -442,7 +449,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { SoundEditionV1_2 edition2 = createGenericEdition(); // Use the same minter for both editions - FixedPriceSignatureMinterV2 minter = new FixedPriceSignatureMinterV2(); + FixedPriceSignatureMinterV2_1 minter = new FixedPriceSignatureMinterV2_1(); edition1.grantRoles(address(minter), edition1.MINTER_ROLE()); edition2.grantRoles(address(minter), edition2.MINTER_ROLE()); @@ -489,7 +496,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { // Mint with same signature on edition 2 fails - signature invalid vm.prank(buyer); - vm.expectRevert(IFixedPriceSignatureMinterV2.InvalidSignature.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.InvalidSignature.selector); minter.mint{ value: PRICE }( address(edition2), mintId2, @@ -502,7 +509,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_signatureCannotBeReusedOnDifferentMintInstances() external { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); address buyer = getFundedAccount(1); @@ -546,7 +553,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { // Mint with same signature on mint instance 2 - signature invalid vm.prank(buyer); - vm.expectRevert(IFixedPriceSignatureMinterV2.InvalidSignature.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.InvalidSignature.selector); minter.mint{ value: PRICE }( address(edition), mintId2, @@ -568,7 +575,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { bool[] memory expectedClaimedAndUnclaimed = new bool[](numOfTokensToBuy * 2); - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint128 mintId = minter.createEditionMint( address(edition), @@ -614,7 +621,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_setMaxMintable(uint32 maxMintable) public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); vm.expectEmit(true, true, true, true); emit MaxMintableSet(address(edition), MINT_ID, maxMintable); @@ -624,7 +631,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_setMaxMintableRevertsIfCallerNotEditionOwnerOrAdmin(uint32 maxMintable) external { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); address attacker = getFundedAccount(1); vm.expectRevert(Ownable.Unauthorized.selector); @@ -633,7 +640,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_setPrice(uint96 price) public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), MINT_ID, price); @@ -645,7 +652,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { function test_setSigner(address signer) public { vm.assume(signer != address(0)); - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); vm.expectEmit(true, true, true, true); emit SignerSet(address(edition), MINT_ID, signer); @@ -655,36 +662,36 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_setZeroSignerReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); - vm.expectRevert(IFixedPriceSignatureMinterV2.SignerIsZeroAddress.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.SignerIsZeroAddress.selector); minter.setSigner(address(edition), MINT_ID, address(0)); } function test_supportsInterface() public { - (, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); - bool supportsIMinterModuleV2 = minter.supportsInterface(type(IMinterModuleV2).interfaceId); - bool supportsIFixedPriceSignatureMinterV2 = minter.supportsInterface( - type(IFixedPriceSignatureMinterV2).interfaceId + bool supportsIMinterModuleV2_1 = minter.supportsInterface(type(IMinterModuleV2_1).interfaceId); + bool supportsIFixedPriceSignatureMinterV2_1 = minter.supportsInterface( + type(IFixedPriceSignatureMinterV2_1).interfaceId ); bool supports165 = minter.supportsInterface(type(IERC165).interfaceId); assertTrue(supports165); - assertTrue(supportsIMinterModuleV2); - assertTrue(supportsIFixedPriceSignatureMinterV2); + assertTrue(supportsIMinterModuleV2_1); + assertTrue(supportsIFixedPriceSignatureMinterV2_1); } function test_moduleInterfaceId() public { - (, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); - assertTrue(type(IFixedPriceSignatureMinterV2).interfaceId == minter.moduleInterfaceId()); + assertTrue(type(IFixedPriceSignatureMinterV2_1).interfaceId == minter.moduleInterfaceId()); } function test_mintInfo() public { SoundEditionV1_2 edition = createGenericEdition(); - FixedPriceSignatureMinterV2 minter = new FixedPriceSignatureMinterV2(); + FixedPriceSignatureMinterV2_1 minter = new FixedPriceSignatureMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -716,7 +723,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintWithDifferentChainIdReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 claimTicket = 0; address buyer = getFundedAccount(1); @@ -726,7 +733,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { // This mint fails because the chain id is different. vm.prank(buyer); - vm.expectRevert(IFixedPriceSignatureMinterV2.InvalidSignature.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.InvalidSignature.selector); vm.chainId(11111); minter.mint{ value: PRICE }( address(edition), @@ -753,7 +760,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { } function test_mintWithMoreThanSignedQuantityReverts() public { - (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2 minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinterV2_1 minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = 2; @@ -765,7 +772,7 @@ contract FixedPriceSignatureMinterV2Tests is TestConfig { // This mint fails because we have exceeded the signed quantity. vm.prank(buyer); - vm.expectRevert(IFixedPriceSignatureMinterV2.ExceedsSignedQuantity.selector); + vm.expectRevert(IFixedPriceSignatureMinterV2_1.ExceedsSignedQuantity.selector); minter.mint{ value: PRICE * quantity }( address(edition), MINT_ID, diff --git a/tests/modules/GoldenEggMetadata.t.sol b/tests/modules/GoldenEggMetadata.t.sol index e70b2505..c58c4f68 100644 --- a/tests/modules/GoldenEggMetadata.t.sol +++ b/tests/modules/GoldenEggMetadata.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.16; import { Strings } from "openzeppelin/utils/Strings.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; import { GoldenEggMetadata } from "@modules/GoldenEggMetadata.sol"; import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; @@ -30,11 +30,11 @@ contract GoldenEggMetadataTests is TestConfig { internal returns ( SoundEditionV1_2 edition, - RangeEditionMinterV2 minter, + RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule ) { - minter = new RangeEditionMinterV2(); + minter = new RangeEditionMinterV2_1(); goldenEggModule = new GoldenEggMetadata(); edition = SoundEditionV1_2( @@ -90,7 +90,7 @@ contract GoldenEggMetadataTests is TestConfig { ) ); - RangeEditionMinterV2 minter = new RangeEditionMinterV2(); + RangeEditionMinterV2_1 minter = new RangeEditionMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -125,7 +125,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test if tokenURI returns default metadata using baseURI, if auction is still active function test_getTokenURIBeforeAuctionEnded() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -141,7 +141,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test if tokenURI returns goldenEgg uri, when max tokens minted function test_getTokenURIAfterMaxMinted() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -158,7 +158,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test if tokenURI returns goldenEgg uri, when both randomnessLocked conditions have been met function test_getTokenURIAfterRandomnessLocked() external { uint32 quantity = MAX_MINTABLE_LOWER - 1; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -185,7 +185,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test if setMintRandomnessTokenThreshold only callable by Edition's owner function test_setMintRandomnessRevertsForNonOwner() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, ) = _createEdition(CUTOFF_TIME); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, ) = _createEdition(CUTOFF_TIME); uint32 quantity = MAX_MINTABLE_LOWER - 1; minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); @@ -198,7 +198,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test when owner lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaOwnerSuccess() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -220,7 +220,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test when admin lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaAdminSuccess() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -262,7 +262,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test when owner lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaOwnerSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( randomnessTimeThreshold ); @@ -286,7 +286,7 @@ contract GoldenEggMetadataTests is TestConfig { // Test when admin lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaAdminSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, GoldenEggMetadata goldenEggModule) = _createEdition( randomnessTimeThreshold ); diff --git a/tests/modules/MerkleDropMinterV2.t.sol b/tests/modules/MerkleDropMinterV2_1.t.sol similarity index 75% rename from tests/modules/MerkleDropMinterV2.t.sol rename to tests/modules/MerkleDropMinterV2_1.t.sol index 7f327dc6..dcc0c704 100644 --- a/tests/modules/MerkleDropMinterV2.t.sol +++ b/tests/modules/MerkleDropMinterV2_1.t.sol @@ -6,13 +6,13 @@ import { Merkle } from "murky/Merkle.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; -import { MerkleDropMinterV2 } from "@modules/MerkleDropMinterV2.sol"; -import { IMerkleDropMinterV2, MintInfo } from "@modules/interfaces/IMerkleDropMinterV2.sol"; -import { IMinterModuleV2 } from "@core/interfaces/IMinterModuleV2.sol"; +import { MerkleDropMinterV2_1 } from "@modules/MerkleDropMinterV2_1.sol"; +import { IMerkleDropMinterV2_1, MintInfo } from "@modules/interfaces/IMerkleDropMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../TestConfig.sol"; -contract MerkleDropMinterV2Tests is TestConfig { +contract MerkleDropMinterV2_1Tests is TestConfig { uint32 public constant START_TIME = 100; uint32 public constant END_TIME = 200; @@ -75,7 +75,7 @@ contract MerkleDropMinterV2Tests is TestConfig { internal returns ( SoundEditionV1_2 edition, - MerkleDropMinterV2 minter, + MerkleDropMinterV2_1 minter, uint128 mintId ) { @@ -83,7 +83,7 @@ contract MerkleDropMinterV2Tests is TestConfig { setUpMerkleTree(); - minter = new MerkleDropMinterV2(); + minter = new MerkleDropMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); mintId = minter.createEditionMint( @@ -100,7 +100,7 @@ contract MerkleDropMinterV2Tests is TestConfig { function test_canMintMultipleTimesLessThanMaxMintablePerAccount() public { uint32 maxPerAccount = 2; - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter( + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter( 0, 6, maxPerAccount @@ -128,7 +128,7 @@ contract MerkleDropMinterV2Tests is TestConfig { function test_cannotClaimMoreThanMaxMintablePerAccount() public { uint32 maxPerAccount = 1; uint32 requestedQuantity = 2; - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter( + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter( 0, 6, maxPerAccount @@ -137,7 +137,7 @@ contract MerkleDropMinterV2Tests is TestConfig { vm.warp(START_TIME); vm.prank(accounts[0]); - vm.expectRevert(IMerkleDropMinterV2.ExceedsMaxPerAccount.selector); + vm.expectRevert(IMerkleDropMinterV2_1.ExceedsMaxPerAccount.selector); // Max is 1 but buyer is requesting 2 minter.mint(address(edition), mintId, requestedQuantity, proof, address(0)); } @@ -146,7 +146,7 @@ contract MerkleDropMinterV2Tests is TestConfig { uint32 maxPerAccount = 3; uint32 requestedQuantity = 3; - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter( + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter( 0, 2, maxPerAccount @@ -155,23 +155,23 @@ contract MerkleDropMinterV2Tests is TestConfig { vm.warp(START_TIME); vm.prank(accounts[2]); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.ExceedsAvailableSupply.selector, 2)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.ExceedsAvailableSupply.selector, 2)); minter.mint(address(edition), mintId, requestedQuantity, proof, address(0)); } function test_cannotClaimWithInvalidProof() public { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 1, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 1, 1); bytes32[] memory proof = m.getProof(leaves, 1); vm.warp(START_TIME); vm.prank(accounts[0]); uint32 requestedQuantity = 1; - vm.expectRevert(IMerkleDropMinterV2.InvalidMerkleProof.selector); + vm.expectRevert(IMerkleDropMinterV2_1.InvalidMerkleProof.selector); minter.mint(address(edition), mintId, requestedQuantity, proof, address(0)); } function test_setPrice(uint96 price) public { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), mintId, price); @@ -181,7 +181,7 @@ contract MerkleDropMinterV2Tests is TestConfig { } function test_setMaxMintable(uint32 maxMintable) public { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit MaxMintableSet(address(edition), mintId, maxMintable); @@ -191,7 +191,7 @@ contract MerkleDropMinterV2Tests is TestConfig { } function test_setMaxMintableRevertsIfCallerNotEditionOwnerOrAdmin(uint32 maxMintable) external { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); address attacker = getFundedAccount(1); vm.expectRevert(Ownable.Unauthorized.selector); @@ -201,7 +201,7 @@ contract MerkleDropMinterV2Tests is TestConfig { function test_setMaxMintablePerAccount(uint32 maxMintablePerAccount) public { vm.assume(maxMintablePerAccount != 0); - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit MaxMintablePerAccountSet(address(edition), mintId, maxMintablePerAccount); @@ -211,7 +211,7 @@ contract MerkleDropMinterV2Tests is TestConfig { } function test_setMaxMintablePerAccountRevertsIfCallerNotEditionOwnerOrAdmin(uint32 maxMintable) external { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); address attacker = getFundedAccount(1); vm.expectRevert(Ownable.Unauthorized.selector); @@ -220,15 +220,15 @@ contract MerkleDropMinterV2Tests is TestConfig { } function test_setMaxMintablePerAccountWithZeroReverts() public { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); - vm.expectRevert(IMerkleDropMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IMerkleDropMinterV2_1.MaxMintablePerAccountIsZero.selector); minter.setMaxMintablePerAccount(address(edition), mintId, 0); } function test_setMerkleRootHash(bytes32 merkleRootHash) public { vm.assume(merkleRootHash != bytes32(0)); - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit MerkleRootHashSet(address(edition), mintId, merkleRootHash); @@ -238,16 +238,16 @@ contract MerkleDropMinterV2Tests is TestConfig { } function test_setEmptyMerkleRootHashReverts() public { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); - vm.expectRevert(IMerkleDropMinterV2.MerkleRootHashIsEmpty.selector); + vm.expectRevert(IMerkleDropMinterV2_1.MerkleRootHashIsEmpty.selector); minter.setMerkleRootHash(address(edition), mintId, bytes32(0)); } function test_setCreateWithMerkleRootHashReverts() public { - (SoundEditionV1_2 edition, MerkleDropMinterV2 minter, ) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinterV2_1 minter, ) = _createEditionAndMinter(0, 0, 1); - vm.expectRevert(IMerkleDropMinterV2.MerkleRootHashIsEmpty.selector); + vm.expectRevert(IMerkleDropMinterV2_1.MerkleRootHashIsEmpty.selector); minter.createEditionMint( address(edition), @@ -262,27 +262,27 @@ contract MerkleDropMinterV2Tests is TestConfig { } function test_supportsInterface() public { - (, MerkleDropMinterV2 minter, ) = _createEditionAndMinter(0, 0, 1); + (, MerkleDropMinterV2_1 minter, ) = _createEditionAndMinter(0, 0, 1); - bool supportsIMinterModuleV2 = minter.supportsInterface(type(IMinterModuleV2).interfaceId); - bool supportsIMerkleDropMint = minter.supportsInterface(type(IMerkleDropMinterV2).interfaceId); + bool supportsIMinterModuleV2_1 = minter.supportsInterface(type(IMinterModuleV2_1).interfaceId); + bool supportsIMerkleDropMint = minter.supportsInterface(type(IMerkleDropMinterV2_1).interfaceId); bool supports165 = minter.supportsInterface(type(IERC165).interfaceId); assertTrue(supports165); - assertTrue(supportsIMinterModuleV2); + assertTrue(supportsIMinterModuleV2_1); assertTrue(supportsIMerkleDropMint); } function test_moduleInterfaceId() public { - (, MerkleDropMinterV2 minter, ) = _createEditionAndMinter(0, 0, 1); + (, MerkleDropMinterV2_1 minter, ) = _createEditionAndMinter(0, 0, 1); - assertTrue(type(IMerkleDropMinterV2).interfaceId == minter.moduleInterfaceId()); + assertTrue(type(IMerkleDropMinterV2_1).interfaceId == minter.moduleInterfaceId()); } function test_mintInfo() public { SoundEditionV1_2 edition = createGenericEdition(); - MerkleDropMinterV2 minter = new MerkleDropMinterV2(); + MerkleDropMinterV2_1 minter = new MerkleDropMinterV2_1(); setUpMerkleTree(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); diff --git a/tests/modules/MintersIntegrationV2.t.sol b/tests/modules/MintersIntegrationV2.t.sol index 4442a7d2..12f66e59 100644 --- a/tests/modules/MintersIntegrationV2.t.sol +++ b/tests/modules/MintersIntegrationV2.t.sol @@ -5,8 +5,8 @@ import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; -import { MerkleDropMinterV2 } from "@modules/MerkleDropMinterV2.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; +import { MerkleDropMinterV2_1 } from "@modules/MerkleDropMinterV2_1.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../TestConfig.sol"; @@ -95,7 +95,7 @@ contract MintersIntegrationV2 is TestConfig { // Setup the Merkle tree (Merkle merkleFreeDrop, bytes32[] memory leavesFreeMerkleDrop) = setUpMerkleTree(accountsFreeMerkleDrop); - MerkleDropMinterV2 merkleDropMinter = new MerkleDropMinterV2(); + MerkleDropMinterV2_1 merkleDropMinter = new MerkleDropMinterV2_1(); edition.grantRoles(address(merkleDropMinter), edition.MINTER_ROLE()); bytes32 root = merkleFreeDrop.getRoot(leavesFreeMerkleDrop); @@ -132,7 +132,7 @@ contract MintersIntegrationV2 is TestConfig { ); // SETUP PUBLIC SALE - RangeEditionMinterV2 publicSaleMinter = new RangeEditionMinterV2(); + RangeEditionMinterV2_1 publicSaleMinter = new RangeEditionMinterV2_1(); edition.grantRoles(address(publicSaleMinter), edition.MINTER_ROLE()); uint128 mintIdPublicSale = publicSaleMinter.createEditionMint( @@ -155,7 +155,7 @@ contract MintersIntegrationV2 is TestConfig { function run_FreeAirdrop( address[] memory accountsFreeMerkleDrop, bytes32[] memory leavesFreeMerkleDrop, - MerkleDropMinterV2 merkleDropMinter, + MerkleDropMinterV2_1 merkleDropMinter, Merkle merkleFreeDrop, uint128 mintId ) public { @@ -194,7 +194,7 @@ contract MintersIntegrationV2 is TestConfig { function run_Presale( address[] memory accountsPresale, bytes32[] memory leavesPresale, - MerkleDropMinterV2 merkleDropMinter, + MerkleDropMinterV2_1 merkleDropMinter, Merkle mPresale, uint128 mintId ) public { @@ -222,7 +222,7 @@ contract MintersIntegrationV2 is TestConfig { vm.warp(START_TIME_PUBLIC_SALE); } - function run_PublicSale(RangeEditionMinterV2 publicSaleMinter, uint128 mintId) public { + function run_PublicSale(RangeEditionMinterV2_1 publicSaleMinter, uint128 mintId) public { // Check user 5 has no tokens uint256 user5Balance = edition.balanceOf(userAccounts[4]); assertEq(user5Balance, 0); diff --git a/tests/modules/OpenGoldenEggMetadata.t.sol b/tests/modules/OpenGoldenEggMetadata.t.sol index ef9b7b9b..1d2f6cb1 100644 --- a/tests/modules/OpenGoldenEggMetadata.t.sol +++ b/tests/modules/OpenGoldenEggMetadata.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.16; import { Strings } from "openzeppelin/utils/Strings.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; import { IOpenGoldenEggMetadata, OpenGoldenEggMetadata } from "@modules/OpenGoldenEggMetadata.sol"; import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { Ownable } from "solady/auth/Ownable.sol"; @@ -32,11 +32,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { internal returns ( SoundEditionV1_2 edition, - RangeEditionMinterV2 minter, + RangeEditionMinterV2_1 minter, OpenGoldenEggMetadata goldenEggModule ) { - minter = new RangeEditionMinterV2(); + minter = new RangeEditionMinterV2_1(); goldenEggModule = new OpenGoldenEggMetadata(); edition = SoundEditionV1_2( @@ -97,7 +97,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { ) ); - RangeEditionMinterV2 minter = new RangeEditionMinterV2(); + RangeEditionMinterV2_1 minter = new RangeEditionMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -142,9 +142,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { tokenId = 1 + (tokenId % mintQuantity); - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - CUTOFF_TIME - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(CUTOFF_TIME); minter.mint{ value: PRICE * mintQuantity }(address(edition), MINT_ID, mintQuantity, address(0)); @@ -178,9 +180,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if tokenURI returns default metadata using baseURI, if auction is still active function test_getTokenURIBeforeAuctionEnded() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - CUTOFF_TIME - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(CUTOFF_TIME); minter.mint{ value: PRICE }(address(edition), MINT_ID, 1, address(0)); uint256 tokenId = 1; @@ -194,9 +198,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if tokenURI returns goldenEgg uri, when max tokens minted function test_getTokenURIAfterMaxMinted() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - CUTOFF_TIME - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(CUTOFF_TIME); uint32 quantity = MAX_MINTABLE_UPPER; @@ -211,9 +217,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if tokenURI returns goldenEgg uri, when both randomnessLocked conditions have been met function test_getTokenURIAfterRandomnessLocked() external { uint32 quantity = MAX_MINTABLE_LOWER - 1; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - CUTOFF_TIME - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(CUTOFF_TIME); minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); @@ -238,7 +246,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if setMintRandomnessTokenThreshold only callable by Edition's owner function test_setMintRandomnessRevertsForNonOwner() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, ) = _createEdition(CUTOFF_TIME); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter, ) = _createEdition(CUTOFF_TIME); uint32 quantity = MAX_MINTABLE_LOWER - 1; minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); @@ -251,9 +259,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test when owner lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaOwnerSuccess() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - CUTOFF_TIME - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(CUTOFF_TIME); uint32 quantity = MAX_MINTABLE_LOWER - 1; minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); @@ -273,9 +283,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test when admin lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaAdminSuccess() external { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - CUTOFF_TIME - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(CUTOFF_TIME); address admin = address(789); edition.grantRoles(admin, edition.ADMIN_ROLE()); @@ -315,9 +327,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test when owner lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaOwnerSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - randomnessTimeThreshold - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(randomnessTimeThreshold); uint32 quantity = MAX_MINTABLE_LOWER; minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); @@ -339,9 +353,11 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test when admin lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaAdminSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( - randomnessTimeThreshold - ); + ( + SoundEditionV1_2 edition, + RangeEditionMinterV2_1 minter, + OpenGoldenEggMetadata goldenEggModule + ) = _createEdition(randomnessTimeThreshold); address admin = address(789); edition.grantRoles(admin, edition.ADMIN_ROLE()); diff --git a/tests/modules/RangeEditionMinterV2.t.sol b/tests/modules/RangeEditionMinterV2_1.t.sol similarity index 77% rename from tests/modules/RangeEditionMinterV2.t.sol rename to tests/modules/RangeEditionMinterV2_1.t.sol index d9dc01d1..366f082c 100644 --- a/tests/modules/RangeEditionMinterV2.t.sol +++ b/tests/modules/RangeEditionMinterV2_1.t.sol @@ -3,14 +3,14 @@ pragma solidity ^0.8.16; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; -import { RangeEditionMinterV2 } from "@modules/RangeEditionMinterV2.sol"; -import { IRangeEditionMinterV2, MintInfo } from "@modules/interfaces/IRangeEditionMinterV2.sol"; -import { IMinterModuleV2 } from "@core/interfaces/IMinterModuleV2.sol"; -import { BaseMinterV2 } from "@modules/BaseMinterV2.sol"; +import { RangeEditionMinterV2_1 } from "@modules/RangeEditionMinterV2_1.sol"; +import { IRangeEditionMinterV2_1, MintInfo } from "@modules/interfaces/IRangeEditionMinterV2_1.sol"; +import { IMinterModuleV2_1 } from "@core/interfaces/IMinterModuleV2_1.sol"; +import { BaseMinterV2_1 } from "@modules/BaseMinterV2_1.sol"; import { Ownable } from "solady/auth/Ownable.sol"; import { TestConfig } from "../TestConfig.sol"; -contract RangeEditionMinterV2Tests is TestConfig { +contract RangeEditionMinterV2_1Tests is TestConfig { uint96 constant PRICE = 1; uint32 constant START_TIME = 100; @@ -82,11 +82,11 @@ contract RangeEditionMinterV2Tests is TestConfig { function _createEditionAndMinter(uint32 _maxMintablePerAccount) internal - returns (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) + returns (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) { edition = createGenericEdition(); - minter = new RangeEditionMinterV2(); + minter = new RangeEditionMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); @@ -129,21 +129,21 @@ contract RangeEditionMinterV2Tests is TestConfig { ) ); - RangeEditionMinterV2 minter = new RangeEditionMinterV2(); + RangeEditionMinterV2_1 minter = new RangeEditionMinterV2_1(); bool hasRevert; if (!(startTime < cutoffTime && cutoffTime < endTime)) { - vm.expectRevert(IMinterModuleV2.InvalidTimeRange.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidTimeRange.selector); hasRevert = true; } else if (!(maxMintableLower <= maxMintableUpper)) { - vm.expectRevert(IRangeEditionMinterV2.InvalidMaxMintableRange.selector); + vm.expectRevert(IRangeEditionMinterV2_1.InvalidMaxMintableRange.selector); hasRevert = true; } else if (maxMintablePerAccount == 0) { - vm.expectRevert(IRangeEditionMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IRangeEditionMinterV2_1.MaxMintablePerAccountIsZero.selector); hasRevert = true; } else if (affiliateFeeBPS > minter.MAX_AFFILIATE_FEE_BPS()) { - vm.expectRevert(IMinterModuleV2.InvalidAffiliateFeeBPS.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidAffiliateFeeBPS.selector); hasRevert = true; } @@ -191,7 +191,7 @@ contract RangeEditionMinterV2Tests is TestConfig { function test_createEditionMintEmitsEvent() public { SoundEditionV1_2 edition = createGenericEdition(); - RangeEditionMinterV2 minter = new RangeEditionMinterV2(); + RangeEditionMinterV2_1 minter = new RangeEditionMinterV2_1(); vm.expectEmit(false, false, false, true); @@ -222,17 +222,17 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_mintWhenOverMaxMintablePerAccountReverts() public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(1); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(1); vm.warp(START_TIME); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(IRangeEditionMinterV2.ExceedsMaxPerAccount.selector); + vm.expectRevert(IRangeEditionMinterV2_1.ExceedsMaxPerAccount.selector); minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0)); } function test_mintWhenOverMaxMintableDueToPreviousMintedReverts() public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(3); vm.warp(START_TIME); address caller = getFundedAccount(1); @@ -245,13 +245,13 @@ contract RangeEditionMinterV2Tests is TestConfig { // attempting to mint 2 more reverts vm.prank(caller); - vm.expectRevert(IRangeEditionMinterV2.ExceedsMaxPerAccount.selector); + vm.expectRevert(IRangeEditionMinterV2_1.ExceedsMaxPerAccount.selector); minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0)); } function test_mintWhenMintablePerAccountIsSetAndSatisfied() public { // Set max allowed per account to 3 - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(3); address caller = getFundedAccount(1); @@ -272,7 +272,7 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_mintUpdatesValuesAndMintsCorrectly() public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.warp(START_TIME); @@ -296,31 +296,31 @@ contract RangeEditionMinterV2Tests is TestConfig { function test_mintRevertForWrongPayment() public { uint32 quantity = 2; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(quantity); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(quantity); vm.warp(START_TIME); uint256 requiredPayment = quantity * PRICE; vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, requiredPayment - 1, requiredPayment) + abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, requiredPayment - 1, requiredPayment) ); minter.mint{ value: requiredPayment - 1 }(address(edition), MINT_ID, quantity, address(0)); vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.WrongPayment.selector, requiredPayment + 1, requiredPayment) + abi.encodeWithSelector(IMinterModuleV2_1.WrongPayment.selector, requiredPayment + 1, requiredPayment) ); minter.mint{ value: requiredPayment + 1 }(address(edition), MINT_ID, quantity, address(0)); } function test_mintRevertsForMintNotOpen() public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); uint32 quantity = 1; vm.warp(START_TIME - 1); vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) + abi.encodeWithSelector(IMinterModuleV2_1.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) ); minter.mint{ value: quantity * PRICE }(address(edition), MINT_ID, quantity, address(0)); @@ -329,7 +329,7 @@ contract RangeEditionMinterV2Tests is TestConfig { vm.warp(END_TIME + 1); vm.expectRevert( - abi.encodeWithSelector(IMinterModuleV2.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) + abi.encodeWithSelector(IMinterModuleV2_1.MintNotOpen.selector, block.timestamp, START_TIME, END_TIME) ); minter.mint{ value: quantity * PRICE }(address(edition), MINT_ID, quantity, address(0)); @@ -341,7 +341,7 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_mintRevertsForSoldOut(uint32 quantityToBuyBeforeCutoff, uint32 quantityToBuyAfterCutoff) public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); quantityToBuyBeforeCutoff = uint32((quantityToBuyBeforeCutoff % uint256(MAX_MINTABLE_UPPER * 2)) + 1); quantityToBuyAfterCutoff = uint32((quantityToBuyAfterCutoff % uint256(MAX_MINTABLE_UPPER * 2)) + 1); @@ -351,7 +351,7 @@ contract RangeEditionMinterV2Tests is TestConfig { if (quantityToBuyBeforeCutoff > MAX_MINTABLE_UPPER) { vm.expectRevert( abi.encodeWithSelector( - IMinterModuleV2.ExceedsAvailableSupply.selector, + IMinterModuleV2_1.ExceedsAvailableSupply.selector, MAX_MINTABLE_UPPER - totalMinted ) ); @@ -368,7 +368,7 @@ contract RangeEditionMinterV2Tests is TestConfig { if (totalMinted + quantityToBuyAfterCutoff > MAX_MINTABLE_LOWER) { uint32 available = MAX_MINTABLE_LOWER > totalMinted ? MAX_MINTABLE_LOWER - totalMinted : 0; - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.ExceedsAvailableSupply.selector, available)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.ExceedsAvailableSupply.selector, available)); } vm.warp(CUTOFF_TIME); minter.mint{ value: quantityToBuyAfterCutoff * PRICE }( @@ -387,7 +387,7 @@ contract RangeEditionMinterV2Tests is TestConfig { function test_mintBeforeAndAfterCutoffTimeBaseCase() public { uint32 quantity = 1; - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(quantity); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(quantity); uint32 maxMintableLower = 0; uint32 maxMintableUpper = 1; minter.setMaxMintableRange(address(edition), MINT_ID, maxMintableLower, maxMintableUpper); @@ -396,12 +396,12 @@ contract RangeEditionMinterV2Tests is TestConfig { minter.mint{ value: quantity * PRICE }(address(edition), MINT_ID, quantity, address(0)); vm.warp(CUTOFF_TIME); - vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2.ExceedsAvailableSupply.selector, maxMintableLower)); + vm.expectRevert(abi.encodeWithSelector(IMinterModuleV2_1.ExceedsAvailableSupply.selector, maxMintableLower)); minter.mint{ value: quantity * PRICE }(address(edition), MINT_ID, quantity, address(0)); } - function test_canSetTimeRangeBaseMinterV2(address nonController) public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + function test_canSetTimeRangeBaseMinterV2_1(address nonController) public { + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.assume(nonController != address(this)); @@ -422,17 +422,17 @@ contract RangeEditionMinterV2Tests is TestConfig { minter.setTimeRange(address(edition), MINT_ID, 456, 789); } - function test_cannotSetInvalidTimeRangeBaseMinterV2(uint32 startTime, uint32 endTime) public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + function test_cannotSetInvalidTimeRangeBaseMinterV2_1(uint32 startTime, uint32 endTime) public { + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); // Ensure startTime cannot be after cutoff time vm.assume(startTime > CUTOFF_TIME); - vm.expectRevert(IMinterModuleV2.InvalidTimeRange.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidTimeRange.selector); minter.setTimeRange(address(edition), MINT_ID, startTime, endTime); // Ensure endTime cannot be before cutoff time vm.assume(endTime < CUTOFF_TIME); - vm.expectRevert(IMinterModuleV2.InvalidTimeRange.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidTimeRange.selector); minter.setTimeRange(address(edition), MINT_ID, startTime, endTime); } @@ -441,11 +441,11 @@ contract RangeEditionMinterV2Tests is TestConfig { uint32 cutoffTime, uint32 endTime ) public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); bool hasRevert; if (!(startTime < cutoffTime && cutoffTime < endTime)) { - vm.expectRevert(IMinterModuleV2.InvalidTimeRange.selector); + vm.expectRevert(IMinterModuleV2_1.InvalidTimeRange.selector); hasRevert = true; } @@ -466,12 +466,12 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_setMaxMintableRange(uint32 maxMintableLower, uint32 maxMintableUpper) public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); bool hasRevert; if (!(maxMintableLower <= maxMintableUpper)) { - vm.expectRevert(IRangeEditionMinterV2.InvalidMaxMintableRange.selector); + vm.expectRevert(IRangeEditionMinterV2_1.InvalidMaxMintableRange.selector); hasRevert = true; } @@ -490,7 +490,7 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_setPrice(uint96 price) public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), MINT_ID, price); @@ -501,7 +501,7 @@ contract RangeEditionMinterV2Tests is TestConfig { function test_setMaxMintablePerAccount(uint32 maxMintablePerAccount) public { vm.assume(maxMintablePerAccount != 0); - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit MaxMintablePerAccountSet(address(edition), MINT_ID, maxMintablePerAccount); @@ -511,16 +511,16 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_setZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - vm.expectRevert(IRangeEditionMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IRangeEditionMinterV2_1.MaxMintablePerAccountIsZero.selector); minter.setMaxMintablePerAccount(address(edition), MINT_ID, 0); } function test_createWithZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_2 edition, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - vm.expectRevert(IRangeEditionMinterV2.MaxMintablePerAccountIsZero.selector); + vm.expectRevert(IRangeEditionMinterV2_1.MaxMintablePerAccountIsZero.selector); minter.createEditionMint( address(edition), PRICE, @@ -535,27 +535,27 @@ contract RangeEditionMinterV2Tests is TestConfig { } function test_supportsInterface() public { - (, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - bool supportsIMinterModuleV2 = minter.supportsInterface(type(IMinterModuleV2).interfaceId); - bool supportsIRangeEditionMinterV2 = minter.supportsInterface(type(IRangeEditionMinterV2).interfaceId); + bool supportsIMinterModuleV2_1 = minter.supportsInterface(type(IMinterModuleV2_1).interfaceId); + bool supportsIRangeEditionMinterV2_1 = minter.supportsInterface(type(IRangeEditionMinterV2_1).interfaceId); bool supports165 = minter.supportsInterface(type(IERC165).interfaceId); assertTrue(supports165); - assertTrue(supportsIRangeEditionMinterV2); - assertTrue(supportsIMinterModuleV2); + assertTrue(supportsIRangeEditionMinterV2_1); + assertTrue(supportsIMinterModuleV2_1); } function test_moduleInterfaceId() public { - (, RangeEditionMinterV2 minter) = _createEditionAndMinter(type(uint32).max); + (, RangeEditionMinterV2_1 minter) = _createEditionAndMinter(type(uint32).max); - assertTrue(type(IRangeEditionMinterV2).interfaceId == minter.moduleInterfaceId()); + assertTrue(type(IRangeEditionMinterV2_1).interfaceId == minter.moduleInterfaceId()); } function test_mintInfo() public { SoundEditionV1_2 edition = createGenericEdition(); - RangeEditionMinterV2 minter = new RangeEditionMinterV2(); + RangeEditionMinterV2_1 minter = new RangeEditionMinterV2_1(); edition.grantRoles(address(minter), edition.MINTER_ROLE()); diff --git a/tests/modules/SAM.t.sol b/tests/modules/SAMV1_1.t.sol similarity index 96% rename from tests/modules/SAM.t.sol rename to tests/modules/SAMV1_1.t.sol index 34756e57..ae170730 100644 --- a/tests/modules/SAM.t.sol +++ b/tests/modules/SAMV1_1.t.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.16; import { Merkle } from "murky/Merkle.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { IERC721AUpgradeable, ISoundEditionV1_2, SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; -import { ISAM, SAM, SAMInfo } from "@modules/SAM.sol"; +import { ISAMV1_1, SAMV1_1, SAMInfo } from "@modules/SAMV1_1.sol"; +import { ISAM } from "@modules/SAM.sol"; import { Ownable } from "solady/auth/Ownable.sol"; import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; import { LibPRNG } from "solady/utils/LibPRNG.sol"; @@ -65,7 +66,7 @@ contract MulticallerWithSenderAttacker { targets[0] = msg.sender; bytes[] memory data = new bytes[](1); - data[0] = abi.encodeWithSelector(ISAM.setAffiliateFee.selector, msg.sender, uint16(12)); + data[0] = abi.encodeWithSelector(ISAMV1_1.setAffiliateFee.selector, msg.sender, uint16(12)); MulticallerWithSender multicallerWithSender = MulticallerWithSender( payable(LibMulticaller.MULTICALLER_WITH_SENDER) @@ -75,7 +76,7 @@ contract MulticallerWithSenderAttacker { } } -contract MockSAM is SAM { +contract MockSAM is SAMV1_1 { bool internal _checkEdition; function directSetPoolBalance(address edition, uint256 balance) public { @@ -300,11 +301,13 @@ contract SAMTests is TestConfig { function test_supportsInterface() public { MockSAM sam = new MockSAM(); + bool supportsISAMV1_1 = sam.supportsInterface(type(ISAMV1_1).interfaceId); bool supportsISAM = sam.supportsInterface(type(ISAM).interfaceId); bool supports165 = sam.supportsInterface(type(IERC165).interfaceId); assertTrue(supports165); assertTrue(supportsISAM); + assertTrue(supportsISAMV1_1); } function _createEditionAndSAM() internal returns (SoundEditionV1_2 edition, MockSAM sam) { @@ -395,11 +398,11 @@ contract SAMTests is TestConfig { sam.buy{ value: 1 }(address(edition), exploiter, 1, address(0), new bytes32[](0)); - vm.expectRevert(ISAM.InSAMPhase.selector); + vm.expectRevert(ISAMV1_1.InSAMPhase.selector); sam.setInflectionPrice(address(edition), 500 ether); - vm.expectRevert(ISAM.InSAMPhase.selector); + vm.expectRevert(ISAMV1_1.InSAMPhase.selector); sam.setInflectionPoint(address(edition), 1); - vm.expectRevert(ISAM.InSAMPhase.selector); + vm.expectRevert(ISAMV1_1.InSAMPhase.selector); sam.setBasePrice(address(edition), 100 ether); uint256[] memory tokenIds = new uint256[](1); @@ -946,7 +949,7 @@ contract SAMTests is TestConfig { _maxMint(edition); - vm.expectRevert(ISAM.SAMDoesNotExist.selector); + vm.expectRevert(ISAMV1_1.SAMDoesNotExist.selector); sam.buy{ value: address(this).balance }(address(edition), address(1), 1, address(0), new bytes32[](0)); } @@ -962,7 +965,7 @@ contract SAMTests is TestConfig { uint256[] memory tokenIdsToSell = new uint256[](1); tokenIdsToSell[0] = 1; - vm.expectRevert(ISAM.SAMDoesNotExist.selector); + vm.expectRevert(ISAMV1_1.SAMDoesNotExist.selector); sam.sell(address(edition), tokenIdsToSell, 0, address(this)); } @@ -1232,7 +1235,7 @@ contract SAMTests is TestConfig { function _testWithdrawForPlatform(MockSAM sam) internal { (address feeAddr, ) = _randomSigner(); - vm.expectRevert(ISAM.PlatformFeeAddressIsZero.selector); + vm.expectRevert(ISAMV1_1.PlatformFeeAddressIsZero.selector); sam.setPlatformFeeAddress(address(0)); vm.expectRevert(Ownable.Unauthorized.selector); @@ -1441,14 +1444,14 @@ contract SAMTests is TestConfig { } if (_random() % 8 == 0) _checkAllExists(edition, collector, t.tokenIds[0]); if (t.tokenIds[0].length == 0) { - vm.expectRevert(ISAM.BurnZeroQuantity.selector); + vm.expectRevert(ISAMV1_1.BurnZeroQuantity.selector); } sam.sell(address(edition), t.tokenIds[0], t.totalSellPrices[0], collector, t.attributionId); if (_random() % 8 == 0) _checkAllBurned(edition, collector, t.tokenIds[0]); if (_random() % 2 == 0) { if (t.tokenIds[1].length == 0) { - vm.expectRevert(ISAM.BurnZeroQuantity.selector); + vm.expectRevert(ISAMV1_1.BurnZeroQuantity.selector); } else { vm.expectRevert( abi.encodeWithSignature( @@ -1476,7 +1479,7 @@ contract SAMTests is TestConfig { vm.warp(block.timestamp + 60); if (_random() % 8 == 0) _checkAllExists(edition, collector, t.tokenIds[1]); if (t.tokenIds[1].length == 0) { - vm.expectRevert(ISAM.BurnZeroQuantity.selector); + vm.expectRevert(ISAMV1_1.BurnZeroQuantity.selector); } sam.sell(address(edition), t.tokenIds[1], t.totalSellPrices[1], collector); if (_random() % 8 == 0) _checkAllBurned(edition, collector, t.tokenIds[1]); @@ -1531,7 +1534,7 @@ contract SAMTests is TestConfig { emit BuyFreezeTimeSet(address(edition), uint32(block.timestamp)); sam.setBuyFreezeTime(address(edition), uint32(block.timestamp)); - vm.expectRevert(ISAM.BuyIsFrozen.selector); + vm.expectRevert(ISAMV1_1.BuyIsFrozen.selector); sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); vm.warp(block.timestamp + 60); @@ -1555,7 +1558,7 @@ contract SAMTests is TestConfig { _mintOut(edition); - vm.expectRevert(ISAM.InvalidBuyFreezeTime.selector); + vm.expectRevert(ISAMV1_1.InvalidBuyFreezeTime.selector); sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime + 11)); sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime + 10)); @@ -1571,7 +1574,7 @@ contract SAMTests is TestConfig { vm.warp(buyFreezeTime); - vm.expectRevert(ISAM.BuyIsFrozen.selector); + vm.expectRevert(ISAMV1_1.BuyIsFrozen.selector); sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); } @@ -1589,7 +1592,7 @@ contract SAMTests is TestConfig { _mintOut(edition); - vm.expectRevert(ISAM.InvalidMaxSupply.selector); + vm.expectRevert(ISAMV1_1.InvalidMaxSupply.selector); sam.setMaxSupply(address(edition), uint32(maxSupply + 11)); sam.setMaxSupply(address(edition), uint32(maxSupply)); @@ -1637,23 +1640,23 @@ contract SAMTests is TestConfig { vm.expectRevert(Ownable.Unauthorized.selector); hasRevert = true; } else if (t.maxSupply == 0) { - vm.expectRevert(ISAM.InvalidMaxSupply.selector); + vm.expectRevert(ISAMV1_1.InvalidMaxSupply.selector); hasRevert = true; } else if (t.buyFreezeTime == 0) { - vm.expectRevert(ISAM.InvalidBuyFreezeTime.selector); + vm.expectRevert(ISAMV1_1.InvalidBuyFreezeTime.selector); hasRevert = true; } else if (t.artistFeeBPS > t.maxArtistFeeBPS) { - vm.expectRevert(ISAM.InvalidArtistFeeBPS.selector); + vm.expectRevert(ISAMV1_1.InvalidArtistFeeBPS.selector); hasRevert = true; } else if (t.goldenEggFeeBPS > t.maxGoldenEggFeeBPS) { - vm.expectRevert(ISAM.InvalidGoldenEggFeeBPS.selector); + vm.expectRevert(ISAMV1_1.InvalidGoldenEggFeeBPS.selector); hasRevert = true; } else if (t.affiliateFeeBPS > t.maxAffiliateFeeBPS) { - vm.expectRevert(ISAM.InvalidAffiliateFeeBPS.selector); + vm.expectRevert(ISAMV1_1.InvalidAffiliateFeeBPS.selector); hasRevert = true; } else if (_random() % 2 == 0) { _maxMint(edition); - vm.expectRevert(ISAM.InSAMPhase.selector); + vm.expectRevert(ISAMV1_1.InSAMPhase.selector); hasRevert = true; } @@ -1695,7 +1698,7 @@ contract SAMTests is TestConfig { // Test if repeated creation for the same edition is not allowed. if (_random() % 2 == 0) { - vm.expectRevert(ISAM.SAMAlreadyExists.selector); + vm.expectRevert(ISAMV1_1.SAMAlreadyExists.selector); sam.create( address(edition), uint96(t.basePrice), @@ -1848,7 +1851,7 @@ contract SAMTests is TestConfig { // the `onlyBeforeSAMPhase` modifier reverts. if (_random() % 16 == 0 && !hasRevert && !(r == 4 || r == 6)) { _mintOut(edition); - vm.expectRevert(ISAM.InSAMPhase.selector); + vm.expectRevert(ISAMV1_1.InSAMPhase.selector); mintHasConcluded = true; hasRevert = true; } @@ -1909,7 +1912,7 @@ contract SAMTests is TestConfig { if (r == 4) { t.artistFeeBPS = _bound(_random(), 0, t.maxArtistFeeBPS * 2); if (t.artistFeeBPS > t.maxArtistFeeBPS && !hasRevert) { - vm.expectRevert(ISAM.InvalidArtistFeeBPS.selector); + vm.expectRevert(ISAMV1_1.InvalidArtistFeeBPS.selector); hasRevert = true; } if (!hasRevert) { @@ -1926,7 +1929,7 @@ contract SAMTests is TestConfig { if (r == 5) { t.goldenEggFeeBPS = _bound(_random(), 0, t.maxGoldenEggFeeBPS * 2); if (t.goldenEggFeeBPS > t.maxGoldenEggFeeBPS && !hasRevert) { - vm.expectRevert(ISAM.InvalidGoldenEggFeeBPS.selector); + vm.expectRevert(ISAMV1_1.InvalidGoldenEggFeeBPS.selector); hasRevert = true; } if (!hasRevert) { @@ -1943,7 +1946,7 @@ contract SAMTests is TestConfig { if (r == 6) { t.affiliateFeeBPS = _bound(_random(), 0, t.maxAffiliateFeeBPS * 2); if (t.affiliateFeeBPS > t.maxAffiliateFeeBPS && !hasRevert) { - vm.expectRevert(ISAM.InvalidAffiliateFeeBPS.selector); + vm.expectRevert(ISAMV1_1.InvalidAffiliateFeeBPS.selector); hasRevert = true; } if (!hasRevert) { @@ -2001,7 +2004,7 @@ contract SAMTests is TestConfig { // Test `buy` to see if it reverts for unapproved affiliate. bytes32[] memory proof = m.getProof(leaves, 1); - vm.expectRevert(ISAM.InvalidAffiliate.selector); + vm.expectRevert(ISAMV1_1.InvalidAffiliate.selector); sam.buy{ value: address(this).balance }(address(edition), address(this), 1, accounts[0], proof); sam.buy{ value: address(this).balance }(address(edition), address(this), 1, accounts[1], proof); @@ -2028,13 +2031,17 @@ contract SAMTests is TestConfig { targets[2] = address(sam); bytes[] memory data = new bytes[](3); - data[0] = abi.encodeWithSelector(ISAM.setBasePrice.selector, address(edition), uint96(t.basePrice)); + data[0] = abi.encodeWithSelector(ISAMV1_1.setBasePrice.selector, address(edition), uint96(t.basePrice)); data[1] = abi.encodeWithSelector( - ISAM.setInflectionPrice.selector, + ISAMV1_1.setInflectionPrice.selector, address(edition), uint128(t.inflectionPrice) ); - data[2] = abi.encodeWithSelector(ISAM.setInflectionPoint.selector, address(edition), uint32(t.inflectionPoint)); + data[2] = abi.encodeWithSelector( + ISAMV1_1.setInflectionPoint.selector, + address(edition), + uint32(t.inflectionPoint) + ); bool isUnauthorized; if (_random() % 16 == 0) { @@ -2055,9 +2062,13 @@ contract SAMTests is TestConfig { assertEq(info.inflectionPrice, t.inflectionPrice); assertEq(info.inflectionPoint, t.inflectionPoint); - data[0] = abi.encodeWithSelector(ISAM.setBasePrice.selector, address(edition), uint96(1 ether)); - data[1] = abi.encodeWithSelector(ISAM.setInflectionPrice.selector, address(edition), uint96(1)); - data[2] = abi.encodeWithSelector(ISAM.setInflectionPoint.selector, address(edition), uint32(type(uint32).max)); + data[0] = abi.encodeWithSelector(ISAMV1_1.setBasePrice.selector, address(edition), uint96(1 ether)); + data[1] = abi.encodeWithSelector(ISAMV1_1.setInflectionPrice.selector, address(edition), uint96(1)); + data[2] = abi.encodeWithSelector( + ISAMV1_1.setInflectionPoint.selector, + address(edition), + uint32(type(uint32).max) + ); multicallerWithSender.aggregateWithSender(targets, data, new uint256[](data.length)); @@ -2088,7 +2099,7 @@ contract SAMTests is TestConfig { targets[0] = address(sam); data = new bytes[](1); - data[0] = abi.encodeWithSelector(ISAM.withdrawForAffiliate.selector, affiliate); + data[0] = abi.encodeWithSelector(ISAMV1_1.withdrawForAffiliate.selector, affiliate); multicallerWithSender.aggregateWithSender(targets, data, new uint256[](data.length)); } @@ -2104,7 +2115,7 @@ contract SAMTests is TestConfig { sam.setCheckEdition(true); - vm.expectRevert(ISAM.UnapprovedEdition.selector); + vm.expectRevert(ISAMV1_1.UnapprovedEdition.selector); sam.create( address(edition), BASE_PRICE, @@ -2165,7 +2176,7 @@ contract SAMTests is TestConfig { edition = createGenericEdition(); - vm.expectRevert(ISAM.UnapprovedEdition.selector); + vm.expectRevert(ISAMV1_1.UnapprovedEdition.selector); sam.create( address(edition), BASE_PRICE,