diff --git a/.changeset/neat-llamas-cough.md b/.changeset/neat-llamas-cough.md new file mode 100644 index 00000000..0d419e5b --- /dev/null +++ b/.changeset/neat-llamas-cough.md @@ -0,0 +1,5 @@ +--- +"@soundxyz/sound-protocol": minor +--- + +Sound Edition V1.2, w/ SAM diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd19ab2a..6dfe47ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,3 @@ jobs: forge test -vvv --gas-report id: test - - name: Code coverage - run: | - forge coverage --report lcov - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: lcov.info diff --git a/.gitmodules b/.gitmodules index 44040a12..d935aafe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,11 +12,6 @@ url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable branch = master ignore = dirty -[submodule "lib/ERC721A-Upgradeable"] - path = lib/ERC721A-Upgradeable - url = https://github.com/chiru-labs/ERC721A-Upgradeable - branch = main - ignore = dirty [submodule "lib/solady"] path = lib/solady url = https://github.com/vectorized/solady @@ -31,3 +26,13 @@ url = https://github.com/vectorized/closedsea branch = main ignore = dirty +[submodule "lib/ERC721A-Upgradeable"] + path = lib/ERC721A-Upgradeable + url = https://github.com/chiru-labs/ERC721A-Upgradeable + branch = 05bd2b9993e632ff898472fb6aec6d698a4c6015 + ignore = dirty +[submodule "lib/multicaller"] + path = lib/multicaller + url = https://github.com/vectorized/multicaller + branch = main + ignore = dirty \ No newline at end of file diff --git a/contracts/core/SoundCreatorV1.sol b/contracts/core/SoundCreatorV1.sol index 08397b7f..d853fa54 100644 --- a/contracts/core/SoundCreatorV1.sol +++ b/contracts/core/SoundCreatorV1.sol @@ -30,14 +30,14 @@ pragma solidity ^0.8.16; import { Clones } from "openzeppelin/proxy/Clones.sol"; import { ISoundCreatorV1 } from "./interfaces/ISoundCreatorV1.sol"; -import { ISoundEditionV1_1 } from "./interfaces/ISoundEditionV1_1.sol"; +import { ISoundEditionV1_2 } from "./interfaces/ISoundEditionV1_2.sol"; import { IMetadataModule } from "./interfaces/IMetadataModule.sol"; import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; /** * @title SoundCreatorV1 - * @notice A factory that deploys minimal proxies of `SoundEditionV1_1.sol`. + * @notice A factory that deploys minimal proxies of `SoundEditionV1_2.sol`. * @dev The proxies are OpenZeppelin's Clones implementation of https://eips.ethereum.org/EIPS/eip-1167 */ contract SoundCreatorV1 is ISoundCreatorV1, OwnableRoles { diff --git a/contracts/core/SoundEditionV1_1.sol b/contracts/core/SoundEditionV1_2.sol similarity index 69% rename from contracts/core/SoundEditionV1_1.sol rename to contracts/core/SoundEditionV1_2.sol index b3ba4e8d..f820d1c8 100644 --- a/contracts/core/SoundEditionV1_1.sol +++ b/contracts/core/SoundEditionV1_2.sol @@ -31,32 +31,34 @@ import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgr import { ERC721AUpgradeable, ERC721AStorage } from "chiru-labs/ERC721A-Upgradeable/ERC721AUpgradeable.sol"; import { ERC721AQueryableUpgradeable } from "chiru-labs/ERC721A-Upgradeable/extensions/ERC721AQueryableUpgradeable.sol"; import { ERC721ABurnableUpgradeable } from "chiru-labs/ERC721A-Upgradeable/extensions/ERC721ABurnableUpgradeable.sol"; -import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; import { IERC2981Upgradeable } from "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { LibString } from "solady/utils/LibString.sol"; +import { LibBitmap } from "solady/utils/LibBitmap.sol"; import { OperatorFilterer } from "closedsea/OperatorFilterer.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; -import { ISoundEditionV1_1, EditionInfo } from "./interfaces/ISoundEditionV1_1.sol"; +import { ISoundEditionV1_2, EditionInfo } from "./interfaces/ISoundEditionV1_2.sol"; import { IMetadataModule } from "./interfaces/IMetadataModule.sol"; import { ArweaveURILib } from "./utils/ArweaveURILib.sol"; import { MintRandomnessLib } from "./utils/MintRandomnessLib.sol"; /** - * @title SoundEditionV1_1 + * @title SoundEditionV1_2 * @notice The Sound Edition contract - a creator-owned, modifiable implementation of ERC721A. */ -contract SoundEditionV1_1 is - ISoundEditionV1_1, +contract SoundEditionV1_2 is + ISoundEditionV1_2, ERC721AQueryableUpgradeable, ERC721ABurnableUpgradeable, OwnableRoles, OperatorFilterer { using ArweaveURILib for ArweaveURILib.URI; + using LibBitmap for LibBitmap.Bitmap; // ============================================================= // CONSTANTS @@ -72,14 +74,6 @@ contract SoundEditionV1_1 is */ uint256 public constant ADMIN_ROLE = _ROLE_0; - /** - * @dev The maximum limit for the mint or airdrop `quantity`. - * Prevents the first-time transfer costs for tokens near the end of large mint batches - * via ERC721A from becoming too expensive due to the need to scan many storage slots. - * See: https://chiru-labs.github.io/ERC721A/#/tips?id=batch-size - */ - uint256 public constant ADDRESS_BATCH_MINT_LIMIT = 255; - /** * @dev Basis points denominator used in fee calculations. */ @@ -95,6 +89,11 @@ contract SoundEditionV1_1 is */ bytes4 private constant _INTERFACE_ID_SOUND_EDITION_V1 = 0x50899e54; + /** + * @dev The interface ID for SoundEdition v1.1.0. + */ + bytes4 private constant _INTERFACE_ID_SOUND_EDITION_V1_1 = 0x425aac3d; + /** * @dev The boolean flag on whether the metadata is frozen. */ @@ -173,12 +172,27 @@ contract SoundEditionV1_1 is */ uint8 private _flags; + /** + * @dev The Sound Automated Market (i.e. bonding curve minter), if any. + */ + address public sam; + + /** + * @dev The total number of tokens minted at the very first use of `samMint`. + */ + uint32 private _totalMintedSnapshot; + + /** + * @dev Whether the `_totalMintedSnapshot` has been initialized. + */ + bool private _totalMintedSnapshotInitialized; + // ============================================================= // PUBLIC / EXTERNAL WRITE FUNCTIONS // ============================================================= /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function initialize( string memory name_, @@ -241,33 +255,45 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 + */ + function setSAM(address sam_) external onlyRolesOrOwner(ADMIN_ROLE) onlyBeforeMintConcluded { + // If there has been any tokens minted, disallow setting + // the SAM to a non-zero address. + // So, as long as the initial mints have not concluded, + // the artist can still unset SAM if they desire. + if (_totalMinted() != 0) + if (sam_ != address(0)) revert MintsAlreadyExist(); + sam = sam_; + emit SAMSet(sam_); + } + + /** + * @inheritdoc ISoundEditionV1_2 */ function mint(address to, uint256 quantity) external payable onlyRolesOrOwner(ADMIN_ROLE | MINTER_ROLE) - requireWithinAddressBatchMintLimit(quantity) requireMintable(quantity) - updatesMintRandomness + updatesMintRandomness(quantity) returns (uint256 fromTokenId) { fromTokenId = _nextTokenId(); // Mint the tokens. Will revert if `quantity` is zero. - _mint(to, quantity); + _batchMint(to, quantity); emit Minted(to, quantity, fromTokenId); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function airdrop(address[] calldata to, uint256 quantity) external onlyRolesOrOwner(ADMIN_ROLE) - requireWithinAddressBatchMintLimit(quantity) requireMintable(to.length * quantity) - updatesMintRandomness + updatesMintRandomness(to.length * quantity) returns (uint256 fromTokenId) { if (to.length == 0) revert NoAddressesToAirdrop(); @@ -279,7 +305,7 @@ contract SoundEditionV1_1 is uint256 toLength = to.length; // Mint the tokens. Will revert if `quantity` is zero. for (uint256 i; i != toLength; ++i) { - _mint(to[i], quantity); + _batchMint(to[i], quantity); } } @@ -287,50 +313,180 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 + */ + function samMint(address to, uint256 quantity) + external + payable + onlySAM + onlyAfterMintConcluded + returns (uint256 fromTokenId) + { + if (!_totalMintedSnapshotInitialized) { + _totalMintedSnapshot = uint32(_totalMinted()); + _totalMintedSnapshotInitialized = true; + } + + fromTokenId = _nextTokenId(); + _batchMint(to, quantity); + + // We don't need to emit an event here, + // as the bonding curve minter will have already emitted a comprehensive event. + } + + /** + * @inheritdoc ISoundEditionV1_2 + */ + function samBurn(address burner, uint256[] calldata tokenIds) external onlySAM onlyAfterMintConcluded { + // We can use unchecked as the length of `tokenIds` is bounded + // to a small number by the max block gas limit. + unchecked { + // For performance, we will directly read and update the storage of ERC721A. + ERC721AStorage.Layout storage layout = ERC721AStorage.layout(); + // The next `tokenId` to be minted (i.e. `_nextTokenId()`). + uint256 stop = layout._currentIndex; + + uint256 burnedBit = 1 << 224; // Bit 224 in a packed ownership represents a burned token in ERC721A. + uint256 n = tokenIds.length; + + // For checking if the `tokenIds` are strictly ascending. + uint256 prevTokenId; + + for (uint256 i; i != n; ) { + uint256 tokenId = tokenIds[i]; + + // Revert `tokenId` is out of bounds. + if (_or(tokenId < _startTokenId(), stop <= tokenId)) revert OwnerQueryForNonexistentToken(); + + // Revert if `tokenIds` is not strictly ascending. + // SoundEdition tokens IDs start from 1, and `prevTokenId` is initially 0, + // so the initial pass of the loop won't revert. + if (tokenId <= prevTokenId) revert TokenIdsNotStrictlyAscending(); + + // The initialized packed ownership slot's value. + uint256 prevOwnershipPacked; + // Scan backwards for an initialized packed ownership slot. + // ERC721A's invariant guarantees that there will always be an initialized slot as long as + // the start of the backwards scan falls within `[_startTokenId() .. _nextTokenId())`. + for (uint256 j = tokenId; (prevOwnershipPacked = layout._packedOwnerships[j]) == 0; ) --j; + + // If the initialized slot is burned, revert. + if (prevOwnershipPacked & burnedBit != 0) revert OwnerQueryForNonexistentToken(); + + // Unpack the `tokenOwner` from bits [0..159] of `prevOwnershipPacked`. + address tokenOwner = address(uint160(prevOwnershipPacked)); + + // Enforce waiting a block before a recently minted or transferred token can be burned. + if (block.timestamp == ((prevOwnershipPacked >> 160) & (2**64 - 1))) revert CannotBurnImmediately(); + + // Check if the burner is either the owner or an approved operator for all the + bool mayBurn = tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + + uint256 offset; + uint256 currTokenId = tokenId; + do { + // Revert if the burner is not authorized to burn the token. + if (!mayBurn) + if (getApproved(currTokenId) != burner) revert TransferCallerNotOwnerNorApproved(); + // Emit the `Transfer` event for burn. + emit Transfer(tokenOwner, address(0), currTokenId); + // Increment `offset` and update `currTokenId`. + currTokenId = tokenId + (++offset); + } while ( + // Neither out of bounds, nor at the end of `tokenIds`. + !_or(currTokenId == stop, i + offset == n) && + // Token ID is sequential. + tokenIds[i + offset] == currTokenId && + // The packed ownership slot is not initialized. + layout._packedOwnerships[currTokenId] == 0 + ); + + // Update the packed ownership for `tokenId` in ERC721A's storage. + // + // Bits Layout: + // - [0..159] `addr` + // - [160..223] `startTimestamp` + // - [224] `burned` + // - [225] `nextInitialized` (optional) + // - [232..255] `extraData` (not used) + layout._packedOwnerships[tokenId] = burnedBit | (block.timestamp << 160) | uint256(uint160(tokenOwner)); + + // If the slot after the mini batch is neither out of bounds, nor initialized. + if (currTokenId != stop) + if (layout._packedOwnerships[currTokenId] == 0) + layout._packedOwnerships[currTokenId] = prevOwnershipPacked; + + // Update the address data in ERC721A's storage. + // - Decrease the token balance for the `tokenOwner` (bits [0..63]). + // - Increase the number burned for the `tokenOwner` (bits [128..191]). + // + // Note that this update has to be in the loop as tokens + // can be burned by an operator that is not the token owner. + layout._packedAddressData[tokenOwner] += (offset << 128) - offset; + + // Advance `i` by `offset`, the number of tokens burned in the mini batch. + i += offset; + + // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. + prevTokenId = currTokenId - 1; + } + // Increase the `_burnCounter` in ERC721A's storage. + layout._burnCounter += n; + } + // We don't need to emit an event here, + // as the bonding curve minter will have already emitted a comprehensive event. + } + + /** + * @inheritdoc ISoundEditionV1_2 */ function withdrawETH() external { uint256 amount = address(this).balance; - SafeTransferLib.safeTransferETH(fundingRecipient, amount); + SafeTransferLib.forceSafeTransferETH(fundingRecipient, amount); emit ETHWithdrawn(fundingRecipient, amount, msg.sender); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function withdrawERC20(address[] calldata tokens) external { unchecked { uint256 n = tokens.length; uint256[] memory amounts = new uint256[](n); for (uint256 i; i != n; ++i) { - uint256 amount = IERC20(tokens[i]).balanceOf(address(this)); - SafeTransferLib.safeTransfer(tokens[i], fundingRecipient, amount); - amounts[i] = amount; + amounts[i] = SafeTransferLib.safeTransferAll(tokens[i], fundingRecipient); } emit ERC20Withdrawn(fundingRecipient, tokens, amounts, msg.sender); } } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ - function setMetadataModule(address metadataModule_) external onlyRolesOrOwner(ADMIN_ROLE) onlyMetadataNotFrozen { + function setMetadataModule(address metadataModule_) + external + onlyRolesOrOwner(ADMIN_ROLE) + onlyMetadataNotFrozen + onlyBeforeMintConcluded + { metadataModule = metadataModule_; emit MetadataModuleSet(metadataModule_); + emitAllMetadataUpdate(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setBaseURI(string memory baseURI_) external onlyRolesOrOwner(ADMIN_ROLE) onlyMetadataNotFrozen { _baseURIStorage.update(baseURI_); emit BaseURISet(baseURI_); + emitAllMetadataUpdate(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setContractURI(string memory contractURI_) external onlyRolesOrOwner(ADMIN_ROLE) onlyMetadataNotFrozen { _contractURIStorage.update(contractURI_); @@ -339,7 +495,7 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function freezeMetadata() external onlyRolesOrOwner(ADMIN_ROLE) onlyMetadataNotFrozen { _flags |= METADATA_IS_FROZEN_FLAG; @@ -347,7 +503,7 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setFundingRecipient(address fundingRecipient_) external onlyRolesOrOwner(ADMIN_ROLE) { if (fundingRecipient_ == address(0)) revert InvalidFundingRecipient(); @@ -356,7 +512,7 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setRoyalty(uint16 royaltyBPS_) external onlyRolesOrOwner(ADMIN_ROLE) onlyValidRoyaltyBPS(royaltyBPS_) { royaltyBPS = royaltyBPS_; @@ -364,23 +520,23 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setEditionMaxMintableRange(uint32 editionMaxMintableLower_, uint32 editionMaxMintableUpper_) external onlyRolesOrOwner(ADMIN_ROLE) + onlyBeforeMintConcluded { - if (mintConcluded()) revert MintHasConcluded(); - uint32 currentTotalMinted = uint32(_totalMinted()); if (currentTotalMinted != 0) { - editionMaxMintableLower_ = uint32(FixedPointMathLib.max(editionMaxMintableLower_, currentTotalMinted)); - - editionMaxMintableUpper_ = uint32(FixedPointMathLib.max(editionMaxMintableUpper_, currentTotalMinted)); - + // If the lower bound is larger than the current stored value, revert. + if (editionMaxMintableLower_ > editionMaxMintableLower) revert InvalidEditionMaxMintableRange(); // If the upper bound is larger than the current stored value, revert. if (editionMaxMintableUpper_ > editionMaxMintableUpper) revert InvalidEditionMaxMintableRange(); + + editionMaxMintableLower_ = uint32(FixedPointMathLib.max(editionMaxMintableLower_, currentTotalMinted)); + editionMaxMintableUpper_ = uint32(FixedPointMathLib.max(editionMaxMintableUpper_, currentTotalMinted)); } // If the lower bound is larger than the upper bound, revert. @@ -393,18 +549,20 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ - function setEditionCutoffTime(uint32 editionCutoffTime_) external onlyRolesOrOwner(ADMIN_ROLE) { - if (mintConcluded()) revert MintHasConcluded(); - + function setEditionCutoffTime(uint32 editionCutoffTime_) + external + onlyRolesOrOwner(ADMIN_ROLE) + onlyBeforeMintConcluded + { editionCutoffTime = editionCutoffTime_; emit EditionCutoffTimeSet(editionCutoffTime_); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setMintRandomnessEnabled(bool mintRandomnessEnabled_) external onlyRolesOrOwner(ADMIN_ROLE) { if (_totalMinted() != 0) revert MintsAlreadyExist(); @@ -417,7 +575,7 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function setOperatorFilteringEnabled(bool operatorFilteringEnabled_) external onlyRolesOrOwner(ADMIN_ROLE) { if (operatorFilteringEnabled() != operatorFilteringEnabled_) { @@ -430,6 +588,13 @@ contract SoundEditionV1_1 is emit OperatorFilteringEnablededSet(operatorFilteringEnabled_); } + /** + * @inheritdoc ISoundEditionV1_2 + */ + function emitAllMetadataUpdate() public { + emit BatchMetadataUpdate(_startTokenId(), _nextTokenId() - 1); + } + /** * @inheritdoc IERC721AUpgradeable */ @@ -492,7 +657,7 @@ contract SoundEditionV1_1 is // ============================================================= /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function editionInfo() external view returns (EditionInfo memory info) { info.baseURI = baseURI(); @@ -517,84 +682,90 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ - function mintRandomness() public view returns (uint256) { - if (mintConcluded() && mintRandomnessEnabled()) { - return uint256(keccak256(abi.encode(_mintRandomness, address(this)))); - } - return 0; + function mintRandomness() public view returns (uint256 result) { + if (mintConcluded()) + if (mintRandomnessEnabled()) { + result = _mintRandomness; + assembly { + mstore(0x00, result) + mstore(0x20, address()) + result := keccak256(0x00, 0x40) + } + } } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function editionMaxMintable() public view returns (uint32) { if (block.timestamp < editionCutoffTime) { return editionMaxMintableUpper; } else { - return uint32(FixedPointMathLib.max(editionMaxMintableLower, _totalMinted())); + uint256 t = _totalMintedSnapshotInitialized ? _totalMintedSnapshot : _totalMinted(); + return uint32(FixedPointMathLib.max(editionMaxMintableLower, t)); } } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function isMetadataFrozen() public view returns (bool) { return _flags & METADATA_IS_FROZEN_FLAG != 0; } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function mintRandomnessEnabled() public view returns (bool) { return _flags & MINT_RANDOMNESS_ENABLED_FLAG != 0; } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function operatorFilteringEnabled() public view returns (bool) { return _operatorFilteringEnabled(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function mintConcluded() public view returns (bool) { - return _totalMinted() == editionMaxMintable(); + return _totalMinted() >= editionMaxMintable(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function nextTokenId() public view returns (uint256) { return _nextTokenId(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function numberMinted(address owner) external view returns (uint256) { return _numberMinted(owner); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function numberBurned(address owner) external view returns (uint256) { return _numberBurned(owner); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function totalMinted() public view returns (uint256) { return _totalMinted(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function totalBurned() public view returns (uint256) { return _totalBurned(); @@ -620,17 +791,18 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function supportsInterface(bytes4 interfaceId) public view - override(ISoundEditionV1_1, ERC721AUpgradeable, IERC721AUpgradeable) + override(ISoundEditionV1_2, ERC721AUpgradeable, IERC721AUpgradeable) returns (bool) { return interfaceId == _INTERFACE_ID_SOUND_EDITION_V1 || - interfaceId == type(ISoundEditionV1_1).interfaceId || + interfaceId == _INTERFACE_ID_SOUND_EDITION_V1_1 || + interfaceId == type(ISoundEditionV1_2).interfaceId || ERC721AUpgradeable.supportsInterface(interfaceId) || interfaceId == _INTERFACE_ID_ERC2981 || interfaceId == this.supportsInterface.selector; @@ -664,14 +836,14 @@ contract SoundEditionV1_1 is } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function baseURI() public view returns (string memory) { return _baseURIStorage.load(); } /** - * @inheritdoc ISoundEditionV1_1 + * @inheritdoc ISoundEditionV1_2 */ function contractURI() public view returns (string memory) { return _contractURIStorage.load(); @@ -681,6 +853,17 @@ contract SoundEditionV1_1 is // INTERNAL / PRIVATE HELPERS // ============================================================= + /** + * @dev Override the `onlyRolesOrOwner` modifier on `OwnableRoles` + * to support multicaller sender forwarding. + */ + modifier onlyRolesOrOwner(uint256 roles) virtual override { + address sender = LibMulticaller.sender(); + if (!hasAnyRole(sender, roles)) + if (sender != owner()) revert Unauthorized(); + _; + } + /** * @dev For operator filtering to be toggled on / off. */ @@ -754,23 +937,40 @@ contract SoundEditionV1_1 is } /** - * @dev Ensures that the `quantity` does not exceed `ADDRESS_BATCH_MINT_LIMIT`. - * @param quantity The number of tokens minted per address. + * @dev Ensures that the caller is the Sound Automated Market (i.e. bonding curve minter). */ - modifier requireWithinAddressBatchMintLimit(uint256 quantity) { - if (quantity > ADDRESS_BATCH_MINT_LIMIT) revert ExceedsAddressBatchMintLimit(); + modifier onlySAM() { + if (msg.sender != sam) revert Unauthorized(); + _; + } + + /** + * @dev Ensures that the mint has not been concluded. + */ + modifier onlyBeforeMintConcluded() { + if (mintConcluded()) revert MintHasConcluded(); + _; + } + + /** + * @dev Ensures that the mint has been concluded. + */ + modifier onlyAfterMintConcluded() { + if (!mintConcluded()) revert MintNotConcluded(); _; } /** * @dev Updates the mint randomness. + * @param totalQuantity The total number of tokens to mint. */ - modifier updatesMintRandomness() { + modifier updatesMintRandomness(uint256 totalQuantity) { if (mintRandomnessEnabled() && !mintConcluded()) { uint256 randomness = _mintRandomness; uint256 newRandomness = MintRandomnessLib.nextMintRandomness( randomness, _totalMinted(), + totalQuantity, editionMaxMintable() ); if (newRandomness != randomness) { @@ -823,4 +1023,32 @@ contract SoundEditionV1_1 is } } } + + /** + * @dev Mints a big batch in mini batches to prevent expensive + * first-time transfer gas costs. + * @param to The address to mint to. + * @param quantity The number of NFTs to mint. + */ + function _batchMint(address to, uint256 quantity) internal { + unchecked { + if (quantity == 0) revert MintZeroQuantity(); + // Mint in mini batches of 32. + uint256 i = quantity % 32; + if (i != 0) _mint(to, i); + while (i != quantity) { + _mint(to, 32); + i += 32; + } + } + } + + /** + * @dev Branchless or. + */ + function _or(bool a, bool b) internal pure returns (bool c) { + assembly { + c := or(a, b) + } + } } diff --git a/contracts/core/SoundFeeRegistry.sol b/contracts/core/SoundFeeRegistry.sol index 91be2684..cf40f783 100644 --- a/contracts/core/SoundFeeRegistry.sol +++ b/contracts/core/SoundFeeRegistry.sol @@ -86,7 +86,7 @@ contract SoundFeeRegistry is ISoundFeeRegistry, OwnableRoles { // ============================================================= /** - * @dev Restricts the sound fee address to be address(0). + * @dev Restricts the sound fee address not be the zero address. * @param soundFeeAddress_ The sound fee address. */ modifier onlyValidSoundFeeAddress(address soundFeeAddress_) { diff --git a/contracts/core/interfaces/ISoundEditionV1_2.sol b/contracts/core/interfaces/ISoundEditionV1_2.sol new file mode 100644 index 00000000..0b117cdb --- /dev/null +++ b/contracts/core/interfaces/ISoundEditionV1_2.sol @@ -0,0 +1,688 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; +import { IERC2981Upgradeable } from "openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import { IERC165Upgradeable } from "openzeppelin-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +import { IMetadataModule } from "./IMetadataModule.sol"; + +/** + * @dev The information pertaining to this edition. + */ +struct EditionInfo { + // Base URI for the tokenId. + string baseURI; + // Contract URI for OpenSea storefront. + string contractURI; + // Name of the collection. + string name; + // Symbol of the collection. + string symbol; + // Address that receives primary and secondary royalties. + address fundingRecipient; + // The current max mintable amount; + uint32 editionMaxMintable; + // The lower limit of the maximum number of tokens that can be minted. + uint32 editionMaxMintableUpper; + // The upper limit of the maximum number of tokens that can be minted. + uint32 editionMaxMintableLower; + // The timestamp (in seconds since unix epoch) after which the + // max amount of tokens mintable will drop from + // `maxMintableUpper` to `maxMintableLower`. + uint32 editionCutoffTime; + // Address of metadata module, address(0x00) if not used. + address metadataModule; + // The current mint randomness value. + uint256 mintRandomness; + // The royalty BPS (basis points). + uint16 royaltyBPS; + // Whether the mint randomness is enabled. + bool mintRandomnessEnabled; + // Whether the mint has concluded. + bool mintConcluded; + // Whether the metadata has been frozen. + bool isMetadataFrozen; + // Next token ID to be minted. + uint256 nextTokenId; + // Total number of tokens burned. + uint256 totalBurned; + // Total number of tokens minted. + uint256 totalMinted; + // Total number of tokens currently in existence. + uint256 totalSupply; +} + +/** + * @title ISoundEditionV1_2 + * @notice The interface for Sound edition contracts. + */ +interface ISoundEditionV1_2 is IERC721AUpgradeable, IERC2981Upgradeable { + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when the metadata module is set. + * @param metadataModule the address of the metadata module. + */ + event MetadataModuleSet(address metadataModule); + + /** + * @dev Emitted when the `baseURI` is set. + * @param baseURI the base URI of the edition. + */ + event BaseURISet(string baseURI); + + /** + * @dev Emitted when the `contractURI` is set. + * @param contractURI The contract URI of the edition. + */ + event ContractURISet(string contractURI); + + /** + * @dev Emitted when the metadata is frozen (e.g.: `baseURI` can no longer be changed). + * @param metadataModule The address of the metadata module. + * @param baseURI The base URI of the edition. + * @param contractURI The contract URI of the edition. + */ + event MetadataFrozen(address metadataModule, string baseURI, string contractURI); + + /** + * @dev Emitted when the `fundingRecipient` is set. + * @param fundingRecipient The address of the funding recipient. + */ + event FundingRecipientSet(address fundingRecipient); + + /** + * @dev Emitted when the `royaltyBPS` is set. + * @param bps The new royalty, measured in basis points. + */ + event RoyaltySet(uint16 bps); + + /** + * @dev Emitted when the edition's maximum mintable token quantity range is set. + * @param editionMaxMintableLower_ The lower limit of the maximum number of tokens that can be minted. + * @param editionMaxMintableUpper_ The upper limit of the maximum number of tokens that can be minted. + */ + event EditionMaxMintableRangeSet(uint32 editionMaxMintableLower_, uint32 editionMaxMintableUpper_); + + /** + * @dev Emitted when the edition's cutoff time set. + * @param editionCutoffTime_ The timestamp. + */ + event EditionCutoffTimeSet(uint32 editionCutoffTime_); + + /** + * @dev Emitted when the `mintRandomnessEnabled` is set. + * @param mintRandomnessEnabled_ The boolean value. + */ + event MintRandomnessEnabledSet(bool mintRandomnessEnabled_); + + /** + * @dev Emitted when the `operatorFilteringEnabled` is set. + * @param operatorFilteringEnabled_ The boolean value. + */ + event OperatorFilteringEnablededSet(bool operatorFilteringEnabled_); + + /** + * @dev Emitted upon initialization. + * @param edition_ The address of the edition. + * @param name_ Name of the collection. + * @param symbol_ Symbol of the collection. + * @param metadataModule_ Address of metadata module, address(0x00) if not used. + * @param baseURI_ Base URI. + * @param contractURI_ Contract URI for OpenSea storefront. + * @param fundingRecipient_ Address that receives primary and secondary royalties. + * @param royaltyBPS_ Royalty amount in bps (basis points). + * @param editionMaxMintableLower_ The lower bound of the max mintable quantity for the edition. + * @param editionMaxMintableUpper_ The upper bound of the max mintable quantity for the edition. + * @param editionCutoffTime_ The timestamp after which `editionMaxMintable` drops from + * `editionMaxMintableUpper` to + * `max(_totalMinted(), editionMaxMintableLower)`. + * @param flags_ The bitwise OR result of the initialization flags. + * See: {METADATA_IS_FROZEN_FLAG} + * See: {MINT_RANDOMNESS_ENABLED_FLAG} + */ + event SoundEditionInitialized( + address indexed edition_, + string name_, + string symbol_, + address metadataModule_, + string baseURI_, + string contractURI_, + address fundingRecipient_, + uint16 royaltyBPS_, + uint32 editionMaxMintableLower_, + uint32 editionMaxMintableUpper_, + uint32 editionCutoffTime_, + uint8 flags_ + ); + + /** + * @dev Emitted upon ETH withdrawal. + * @param recipient The recipient of the withdrawal. + * @param amount The amount withdrawn. + * @param caller The account that initiated the withdrawal. + */ + event ETHWithdrawn(address recipient, uint256 amount, address caller); + + /** + * @dev Emitted upon ERC20 withdrawal. + * @param recipient The recipient of the withdrawal. + * @param tokens The addresses of the ERC20 tokens. + * @param amounts The amount of each token withdrawn. + * @param caller The account that initiated the withdrawal. + */ + event ERC20Withdrawn(address recipient, address[] tokens, uint256[] amounts, address caller); + + /** + * @dev Emitted upon a mint. + * @param to The address to mint to. + * @param quantity The number of minted. + * @param fromTokenId The first token ID minted. + */ + event Minted(address to, uint256 quantity, uint256 fromTokenId); + + /** + * @dev Emitted upon an airdrop. + * @param to The recipients of the airdrop. + * @param quantity The number of tokens airdropped to each address in `to`. + * @param fromTokenId The first token ID minted to the first address in `to`. + */ + event Airdropped(address[] to, uint256 quantity, uint256 fromTokenId); + + /** + * @dev EIP-4906 event to signal marketplaces to refresh the metadata. + * @param fromTokenId The starting token ID. + * @param toTokenId The ending token ID. + */ + event BatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId); + + /** + * @dev Emiited when the Sound Automated Market (i.e. bonding curve minter) is set. + * @param sam_ The Sound Automated Market. + */ + event SAMSet(address sam_); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The edition's metadata is frozen (e.g.: `baseURI` can no longer be changed). + */ + error MetadataIsFrozen(); + + /** + * @dev The given `royaltyBPS` is invalid. + */ + error InvalidRoyaltyBPS(); + + /** + * @dev The given `randomnessLockedAfterMinted` value is invalid. + */ + error InvalidRandomnessLock(); + + /** + * @dev The requested quantity exceeds the edition's remaining mintable token quantity. + * @param available The number of tokens remaining available for mint. + */ + error ExceedsEditionAvailableSupply(uint32 available); + + /** + * @dev The given amount is invalid. + */ + error InvalidAmount(); + + /** + * @dev The given `fundingRecipient` address is invalid. + */ + error InvalidFundingRecipient(); + + /** + * @dev The `editionMaxMintableLower` must not be greater than `editionMaxMintableUpper`. + */ + error InvalidEditionMaxMintableRange(); + + /** + * @dev The `editionMaxMintable` has already been reached. + */ + error MaximumHasAlreadyBeenReached(); + + /** + * @dev The mint `quantity` cannot exceed `ADDRESS_BATCH_MINT_LIMIT` tokens. + */ + error ExceedsAddressBatchMintLimit(); + + /** + * @dev The mint randomness has already been revealed. + */ + error MintRandomnessAlreadyRevealed(); + + /** + * @dev No addresses to airdrop. + */ + error NoAddressesToAirdrop(); + + /** + * @dev The mint has already concluded. + */ + error MintHasConcluded(); + + /** + * @dev The mint has not concluded. + */ + error MintNotConcluded(); + + /** + * @dev Cannot perform the operation after a token has been minted. + */ + error MintsAlreadyExist(); + + /** + * @dev The token IDs must be in strictly ascending order. + */ + error TokenIdsNotStrictlyAscending(); + + /** + * @dev Please wait for a while before you burn. + */ + error CannotBurnImmediately(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Initializes the contract. + * @param name_ Name of the collection. + * @param symbol_ Symbol of the collection. + * @param metadataModule_ Address of metadata module, address(0x00) if not used. + * @param baseURI_ Base URI. + * @param contractURI_ Contract URI for OpenSea storefront. + * @param fundingRecipient_ Address that receives primary and secondary royalties. + * @param royaltyBPS_ Royalty amount in bps (basis points). + * @param editionMaxMintableLower_ The lower bound of the max mintable quantity for the edition. + * @param editionMaxMintableUpper_ The upper bound of the max mintable quantity for the edition. + * @param editionCutoffTime_ The timestamp after which `editionMaxMintable` drops from + * `editionMaxMintableUpper` to + * `max(_totalMinted(), editionMaxMintableLower)`. + * @param flags_ The bitwise OR result of the initialization flags. + * See: {METADATA_IS_FROZEN_FLAG} + * See: {MINT_RANDOMNESS_ENABLED_FLAG} + */ + function initialize( + string memory name_, + string memory symbol_, + address metadataModule_, + string memory baseURI_, + string memory contractURI_, + address fundingRecipient_, + uint16 royaltyBPS_, + uint32 editionMaxMintableLower_, + uint32 editionMaxMintableUpper_, + uint32 editionCutoffTime_, + uint8 flags_ + ) external; + + /** + * @dev Mints `quantity` tokens to addrress `to` + * Each token will be assigned a token ID that is consecutively increasing. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have either the + * `ADMIN_ROLE`, `MINTER_ROLE`, which can be granted via {grantRole}. + * Multiple minters, such as different minter contracts, + * can be authorized simultaneously. + * + * @param to Address to mint to. + * @param quantity Number of tokens to mint. + * @return fromTokenId The first token ID minted. + */ + function mint(address to, uint256 quantity) external payable returns (uint256 fromTokenId); + + /** + * @dev Mints `quantity` tokens to each of the addresses in `to`. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the + * `ADMIN_ROLE`, which can be granted via {grantRole}. + * + * @param to Address to mint to. + * @param quantity Number of tokens to mint. + * @return fromTokenId The first token ID minted. + */ + function airdrop(address[] calldata to, uint256 quantity) external returns (uint256 fromTokenId); + + /** + * @dev Mints `quantity` tokens to addrress `to` + * Each token will be assigned a token ID that is consecutively increasing. + * + * Calling conditions: + * - The caller must be the bonding curve contract. + * + * @param to Address to mint to. + * @param quantity Number of tokens to mint. + * @return fromTokenId The first token ID minted. + */ + function samMint(address to, uint256 quantity) external payable returns (uint256 fromTokenId); + + /** + * @dev Burns the `tokenIds`. + * + * Calling conditions: + * - The caller must be the bonding curve contract. + * + * @param burner The initiator of the burn. + * @param tokenIds The list of token IDs to burn. + */ + function samBurn(address burner, uint256[] calldata tokenIds) external; + + /** + * @dev Withdraws collected ETH royalties to the fundingRecipient. + */ + function withdrawETH() external; + + /** + * @dev Withdraws collected ERC20 royalties to the fundingRecipient. + * @param tokens array of ERC20 tokens to withdraw + */ + function withdrawERC20(address[] calldata tokens) external; + + /** + * @dev Sets metadata module. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param metadataModule Address of metadata module. + */ + function setMetadataModule(address metadataModule) external; + + /** + * @dev Sets global base URI. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param baseURI The base URI to be set. + */ + function setBaseURI(string memory baseURI) external; + + /** + * @dev Sets contract URI. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param contractURI The contract URI to be set. + */ + function setContractURI(string memory contractURI) external; + + /** + * @dev Freezes metadata by preventing any more changes to base URI. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + */ + function freezeMetadata() external; + + /** + * @dev Sets funding recipient address. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param fundingRecipient Address to be set as the new funding recipient. + */ + function setFundingRecipient(address fundingRecipient) external; + + /** + * @dev Sets royalty amount in bps (basis points). + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param bps The new royalty basis points to be set. + */ + function setRoyalty(uint16 bps) external; + + /** + * @dev Sets the edition max mintable range. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param editionMaxMintableLower_ The lower limit of the maximum number of tokens that can be minted. + * @param editionMaxMintableUpper_ The upper limit of the maximum number of tokens that can be minted. + */ + function setEditionMaxMintableRange(uint32 editionMaxMintableLower_, uint32 editionMaxMintableUpper_) external; + + /** + * @dev Sets the timestamp after which, the `editionMaxMintable` drops + * from `editionMaxMintableUpper` to `editionMaxMintableLower. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param editionCutoffTime_ The timestamp. + */ + function setEditionCutoffTime(uint32 editionCutoffTime_) external; + + /** + * @dev Sets whether the `mintRandomness` is enabled. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param mintRandomnessEnabled_ The boolean value. + */ + function setMintRandomnessEnabled(bool mintRandomnessEnabled_) external; + + /** + * @dev Sets whether OpenSea operator filtering is enabled. + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param operatorFilteringEnabled_ The boolean value. + */ + function setOperatorFilteringEnabled(bool operatorFilteringEnabled_) external; + + /** + * @dev Emits an event to signal to marketplaces to refresh all the metadata. + */ + function emitAllMetadataUpdate() external; + + /** + * @dev Sets the Sound Automated Market (i.e. bonding curve minter). + * + * Calling conditions: + * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`. + * + * @param sam_ The Sound Automated Market. + */ + function setSAM(address sam_) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the edition info. + * @return editionInfo The latest value. + */ + function editionInfo() external view returns (EditionInfo memory editionInfo); + + /** + * @dev Returns the minter role flag. + * @return The constant value. + */ + function MINTER_ROLE() external view returns (uint256); + + /** + * @dev Returns the admin role flag. + * @return The constant value. + */ + function ADMIN_ROLE() external view returns (uint256); + + /** + * @dev Returns the bit flag to freeze the metadata on initialization. + * @return The constant value. + */ + function METADATA_IS_FROZEN_FLAG() external pure returns (uint8); + + /** + * @dev Returns the bit flag to enable the mint randomness feature on initialization. + * @return The constant value. + */ + function MINT_RANDOMNESS_ENABLED_FLAG() external pure returns (uint8); + + /** + * @dev Returns the bit flag to enable OpenSea operator filtering. + * @return The constant value. + */ + function OPERATOR_FILTERING_ENABLED_FLAG() external pure returns (uint8); + + /** + * @dev Returns the base token URI for the collection. + * @return The configured value. + */ + function baseURI() external view returns (string memory); + + /** + * @dev Returns the contract URI to be used by Opensea. + * See: https://docs.opensea.io/docs/contract-level-metadata + * @return The configured value. + */ + function contractURI() external view returns (string memory); + + /** + * @dev Returns the address of the funding recipient. + * @return The configured value. + */ + function fundingRecipient() external view returns (address); + + /** + * @dev Returns the maximum amount of tokens mintable for this edition. + * @return The configured value. + */ + function editionMaxMintable() external view returns (uint32); + + /** + * @dev Returns the upper bound for the maximum tokens that can be minted for this edition. + * @return The configured value. + */ + function editionMaxMintableUpper() external view returns (uint32); + + /** + * @dev Returns the lower bound for the maximum tokens that can be minted for this edition. + * @return The configured value. + */ + function editionMaxMintableLower() external view returns (uint32); + + /** + * @dev Returns the timestamp after which `editionMaxMintable` drops from + * `editionMaxMintableUpper` to `editionMaxMintableLower`. + * @return The configured value. + */ + function editionCutoffTime() external view returns (uint32); + + /** + * @dev Returns the address of the metadata module. + * @return The configured value. + */ + function metadataModule() external view returns (address); + + /** + * @dev Returns the randomness based on latest block hash, which is stored upon each mint. + * unless {mintConcluded} is true. + * Used for game mechanics like the Sound Golden Egg. + * Returns 0 before revealed. + * WARNING: This value should NOT be used for any reward of significant monetary + * value, due to it being computed via a purely on-chain psuedorandom mechanism. + * @return The latest value. + */ + function mintRandomness() external view returns (uint256); + + /** + * @dev Returns whether the `mintRandomness` has been enabled. + * @return The configured value. + */ + function mintRandomnessEnabled() external view returns (bool); + + /** + * @dev Returns whether the `operatorFilteringEnabled` has been enabled. + * @return The configured value. + */ + function operatorFilteringEnabled() external view returns (bool); + + /** + * @dev Returns whether the mint has been concluded. + * @return The latest value. + */ + function mintConcluded() external view returns (bool); + + /** + * @dev Returns the royalty basis points. + * @return The configured value. + */ + function royaltyBPS() external view returns (uint16); + + /** + * @dev Returns whether the metadata module is frozen. + * @return The configured value. + */ + function isMetadataFrozen() external view returns (bool); + + /** + * @dev Returns the sound automated market, if any. + * @return The configured value. + */ + function sam() external view returns (address); + + /** + * @dev Returns the next token ID to be minted. + * @return The latest value. + */ + function nextTokenId() external view returns (uint256); + + /** + * @dev Returns the number of tokens minted by `owner`. + * @param owner Address to query for number minted. + * @return The latest value. + */ + function numberMinted(address owner) external view returns (uint256); + + /** + * @dev Returns the number of tokens burned by `owner`. + * @param owner Address to query for number burned. + * @return The latest value. + */ + function numberBurned(address owner) external view returns (uint256); + + /** + * @dev Returns the total amount of tokens minted. + * @return The latest value. + */ + function totalMinted() external view returns (uint256); + + /** + * @dev Returns the total amount of tokens burned. + * @return The latest value. + */ + function totalBurned() external view returns (uint256); + + /** + * @dev Informs other contracts which interfaces this contract supports. + * Required by https://eips.ethereum.org/EIPS/eip-165 + * @param interfaceId The interface id to check. + * @return Whether the `interfaceId` is supported. + */ + function supportsInterface(bytes4 interfaceId) + external + view + override(IERC721AUpgradeable, IERC165Upgradeable) + returns (bool); +} diff --git a/contracts/core/utils/MintRandomnessLib.sol b/contracts/core/utils/MintRandomnessLib.sol index bf54fffd..2c000c30 100644 --- a/contracts/core/utils/MintRandomnessLib.sol +++ b/contracts/core/utils/MintRandomnessLib.sol @@ -32,38 +32,52 @@ library MintRandomnessLib { * @dev Returns the next mint randomness. * @param randomness The current mint randomness. * @param totalMinted The total number of tokens minted. + * @param quantity The number of tokens to mint. * @param maxMintable The maximum number of tokens that can be minted. * @return newRandomness The next mint randomness. */ function nextMintRandomness( uint256 randomness, uint256 totalMinted, + uint256 quantity, uint256 maxMintable ) internal view returns (uint256 newRandomness) { assembly { newRandomness := randomness - // Pick any of the last 256 blocks psuedorandomly 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. - // We also need to xor with the `totalMinted` to prevent the randomness - // from being stucked. - mstore(0x20, xor(xor(randomness, difficulty()), totalMinted)) + // If neither `maxMintable` nor `quantity` is zero. + if mul(maxMintable, quantity) { + let end := add(totalMinted, quantity) + // prettier-ignore + for {} 1 {} { + // 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. + // We also need to xor with the `totalMinted` to prevent the randomness + // from being stucked. + mstore(0x20, xor(xor(randomness, difficulty()), totalMinted)) - let r := keccak256(0x00, 0x40) + let r := keccak256(0x00, 0x40) - switch randomness - case 0 { - // If `randomness` is uninitialized, - // initialize all bits psuedorandomly. - newRandomness := r - } - default { - // Decay the chance to update as more are minted. - if gt(mod(r, add(maxMintable, 1)), totalMinted) { - // If `randomness` has already been initialized, - // each update can only contribute 1 bit of psuedorandomness. - newRandomness := or(shl(1, randomness), shr(255, r)) + switch randomness + case 0 { + // If `randomness` is uninitialized, + // initialize all bits pseudorandomly. + newRandomness := r + } + default { + // Decay the chance to update as more are minted. + if gt(add(mod(r, maxMintable), 1), totalMinted) { + // If `randomness` has already been initialized, + // each update can only contribute 1 bit of pseudorandomness. + mstore(0x00, or(shl(1, randomness), shr(255, r))) + newRandomness := keccak256(0x00, 0x20) + } + } + randomness := newRandomness + totalMinted := add(totalMinted, 1) + // prettier-ignore + if eq(totalMinted, end) { break } } } } diff --git a/contracts/modules/SAM.sol b/contracts/modules/SAM.sol new file mode 100644 index 00000000..48435952 --- /dev/null +++ b/contracts/modules/SAM.sol @@ -0,0 +1,918 @@ +// 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 { ISAM, SAMInfo } from "./interfaces/ISAM.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 SAM is ISAM, 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; + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev How much platform fees have been accrued. + */ + uint128 public platformFeesAccrued; + + /** + * @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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + function setBasePrice(address edition, uint96 basePrice) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.basePrice = basePrice; + emit BasePriceSet(edition, basePrice); + } + + /** + * @inheritdoc ISAM + */ + function setLinearPriceSlope(address edition, uint128 linearPriceSlope) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.linearPriceSlope = linearPriceSlope; + emit LinearPriceSlopeSet(edition, linearPriceSlope); + } + + /** + * @inheritdoc ISAM + */ + function setInflectionPrice(address edition, uint128 inflectionPrice) + public + onlyEditionOwnerOrAdmin(edition) + onlyBeforeSAMPhase(edition) + { + SAMData storage data = _getSAMData(edition); + data.inflectionPrice = inflectionPrice; + emit InflectionPriceSet(edition, inflectionPrice); + } + + /** + * @inheritdoc ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + function setPlatformFee(uint16 bps) public onlyOwner { + if (bps > MAX_PLATFORM_FEE_BPS) revert InvalidPlatformFeeBPS(); + platformFeeBPS = bps; + emit PlatformFeeSet(bps); + } + + /** + * @inheritdoc ISAM + */ + function setPlatformFeeAddress(address addr) public onlyOwner { + if (addr == address(0)) revert PlatformFeeAddressIsZero(); + platformFeeAddress = addr; + emit PlatformFeeAddressSet(addr); + } + + /** + * @inheritdoc ISAM + */ + function setApprovedEditionFactories(address[] calldata factories) public onlyOwner { + _approvedEditionFactories = factories; + emit ApprovedEditionFactoriesSet(factories); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ISAM + */ + 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 ISAM + */ + function totalValue( + address edition, + uint32 fromSupply, + uint32 quantity + ) public view returns (uint256 total) { + total = _subTotal(_getSAMData(edition), fromSupply, quantity); + } + + /** + * @inheritdoc ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + 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 ISAM + */ + function goldenEggFeesAccrued(address edition) public view returns (uint128) { + return _getSAMData(edition).goldenEggFeesAccrued; + } + + /** + * @inheritdoc ISAM + */ + 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 ISAM + */ + function isAffiliated(address edition, address affiliate) public view returns (bool) { + return isAffiliatedWithProof(edition, affiliate, MerkleProofLib.emptyProof()); + } + + /** + * @inheritdoc ISAM + */ + function affiliateMerkleRoot(address edition) external view returns (bytes32) { + return _getSAMData(edition).affiliateMerkleRoot; + } + + /** + * @inheritdoc ISAM + */ + 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 == type(ISAM).interfaceId; + } + + /** + * @inheritdoc ISAM + */ + function moduleInterfaceId() public pure returns (bytes4) { + return type(ISAM).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; + 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/ISAM.sol b/contracts/modules/interfaces/ISAM.sol new file mode 100644 index 00000000..cba5dc3c --- /dev/null +++ b/contracts/modules/interfaces/ISAM.sol @@ -0,0 +1,774 @@ +// 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 ISAM 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 `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 `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 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 Returns the platform fee basis points. + * @return The configured value. + */ + function platformFeeBPS() external returns (uint16); + + /** + * @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. + * @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/contracts/modules/utils/BondingCurveLib.sol b/contracts/modules/utils/BondingCurveLib.sol new file mode 100644 index 00000000..5cc81550 --- /dev/null +++ b/contracts/modules/utils/BondingCurveLib.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "solady/utils/FixedPointMathLib.sol"; + +library BondingCurveLib { + function sigmoid2Sum( + uint32 inflectionPoint, + uint128 inflectionPrice, + uint32 fromSupply, + uint32 quantity + ) internal pure returns (uint256 sum) { + // We don't need checked arithmetic for the sum. + // The max possible sum for the quadratic region is capped at: + // `n * (n + 1) * (2*n + 1) * h < 2**32 * 2**33 * 2**34 * 2**128 = 2**227`. + // The max possible sum for the sqrt region is capped at: + // `end * (2*h * sqrt(end)) < 2**32 * 2**129 * 2**16 = 2**177`. + // The overall sum is capped by: + // `2**161 + 2**227 <= 2**228 < 2 **256`. + // The result will be small enough for unchecked multiplication with a 16-bit BPS. + unchecked { + uint256 g = inflectionPoint; + uint256 h = inflectionPrice; + + // Early return to save gas if either `g` or `h` is zero. + if (g * h == 0) return 0; + + uint256 s = uint256(fromSupply) + 1; + uint256 end = s + uint256(quantity); + uint256 quadraticEnd = FixedPointMathLib.min(g, end); + + if (s < quadraticEnd) { + uint256 k = uint256(fromSupply); // `s - 1`. + uint256 n = quadraticEnd - 1; + // In practice, `h` (units: wei) will be set to be much greater than `g * g`. + uint256 a = FixedPointMathLib.rawDiv(h, g * g); + // Use the closed form to compute the sum. + sum = ((n * (n + 1) * ((n << 1) + 1) - k * (k + 1) * ((k << 1) + 1)) / 6) * a; + s = quadraticEnd; + } + + if (s < end) { + uint256 c = (3 * g) >> 2; + uint256 h2 = h << 1; + do { + uint256 r = FixedPointMathLib.sqrt((s - c) * g); + sum += FixedPointMathLib.rawDiv(h2 * r, g); + } while (++s != end); + } + } + } + + function linearSum( + uint128 linearPriceSlope, + uint32 fromSupply, + uint32 quantity + ) internal pure returns (uint256 sum) { + // We don't need checked arithmetic for the sum because the max possible + // intermediate value is capped at: + // `k * m < 2**32 * 2**128 = 2**160 < 2**256`. + // As `quantity` is 32 bits, max possible value for `sum` + // is capped at: + // `2**32 * 2**160 = 2**192 < 2**256`. + // The result will be small enough for unchecked multiplication with a 16-bit BPS. + unchecked { + uint256 m = linearPriceSlope; + uint256 k = uint256(fromSupply); + uint256 n = k + uint256(quantity); + // Use the closed form to compute the sum. + return m * ((n * (n + 1) - k * (k + 1)) >> 1); + } + } +} diff --git a/foundry.toml b/foundry.toml index 8b4f2ef8..895b9647 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,8 @@ libs = ['lib'] test = 'tests' optimizer = true optimizer_runs = 1_000 +solc_version = '0.8.19' + gas_limit = 100_000_000 # ETH is 30M, but we use a higher value. # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/input.json b/input.json new file mode 100644 index 00000000..bfcc847c --- /dev/null +++ b/input.json @@ -0,0 +1 @@ +{"language":"Solidity","sources":{"contracts/core/interfaces/IMetadataModule.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.16;\n\n/**\n * @title IMetadataModule\n * @notice The interface for custom metadata modules.\n */\ninterface IMetadataModule {\n /**\n * @dev When implemented, SoundEdition's `tokenURI` redirects execution to this `tokenURI`.\n * @param tokenId The token ID to retrieve the token URI for.\n * @return The token URI string.\n */\n function tokenURI(uint256 tokenId) external view returns (string memory);\n}\n"},"contracts/core/interfaces/ISoundEditionV1_2.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.16;\n\nimport { IERC721AUpgradeable } from \"chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol\";\nimport { IERC2981Upgradeable } from \"openzeppelin-upgradeable/interfaces/IERC2981Upgradeable.sol\";\nimport { IERC165Upgradeable } from \"openzeppelin-upgradeable/utils/introspection/IERC165Upgradeable.sol\";\n\nimport { IMetadataModule } from \"./IMetadataModule.sol\";\n\n/**\n * @dev The information pertaining to this edition.\n */\nstruct EditionInfo {\n // Base URI for the tokenId.\n string baseURI;\n // Contract URI for OpenSea storefront.\n string contractURI;\n // Name of the collection.\n string name;\n // Symbol of the collection.\n string symbol;\n // Address that receives primary and secondary royalties.\n address fundingRecipient;\n // The current max mintable amount;\n uint32 editionMaxMintable;\n // The lower limit of the maximum number of tokens that can be minted.\n uint32 editionMaxMintableUpper;\n // The upper limit of the maximum number of tokens that can be minted.\n uint32 editionMaxMintableLower;\n // The timestamp (in seconds since unix epoch) after which the\n // max amount of tokens mintable will drop from\n // `maxMintableUpper` to `maxMintableLower`.\n uint32 editionCutoffTime;\n // Address of metadata module, address(0x00) if not used.\n address metadataModule;\n // The current mint randomness value.\n uint256 mintRandomness;\n // The royalty BPS (basis points).\n uint16 royaltyBPS;\n // Whether the mint randomness is enabled.\n bool mintRandomnessEnabled;\n // Whether the mint has concluded.\n bool mintConcluded;\n // Whether the metadata has been frozen.\n bool isMetadataFrozen;\n // Next token ID to be minted.\n uint256 nextTokenId;\n // Total number of tokens burned.\n uint256 totalBurned;\n // Total number of tokens minted.\n uint256 totalMinted;\n // Total number of tokens currently in existence.\n uint256 totalSupply;\n}\n\n/**\n * @title ISoundEditionV1_2\n * @notice The interface for Sound edition contracts.\n */\ninterface ISoundEditionV1_2 is IERC721AUpgradeable, IERC2981Upgradeable {\n // =============================================================\n // EVENTS\n // =============================================================\n\n /**\n * @dev Emitted when the metadata module is set.\n * @param metadataModule the address of the metadata module.\n */\n event MetadataModuleSet(address metadataModule);\n\n /**\n * @dev Emitted when the `baseURI` is set.\n * @param baseURI the base URI of the edition.\n */\n event BaseURISet(string baseURI);\n\n /**\n * @dev Emitted when the `contractURI` is set.\n * @param contractURI The contract URI of the edition.\n */\n event ContractURISet(string contractURI);\n\n /**\n * @dev Emitted when the metadata is frozen (e.g.: `baseURI` can no longer be changed).\n * @param metadataModule The address of the metadata module.\n * @param baseURI The base URI of the edition.\n * @param contractURI The contract URI of the edition.\n */\n event MetadataFrozen(address metadataModule, string baseURI, string contractURI);\n\n /**\n * @dev Emitted when the `fundingRecipient` is set.\n * @param fundingRecipient The address of the funding recipient.\n */\n event FundingRecipientSet(address fundingRecipient);\n\n /**\n * @dev Emitted when the `royaltyBPS` is set.\n * @param bps The new royalty, measured in basis points.\n */\n event RoyaltySet(uint16 bps);\n\n /**\n * @dev Emitted when the edition's maximum mintable token quantity range is set.\n * @param editionMaxMintableLower_ The lower limit of the maximum number of tokens that can be minted.\n * @param editionMaxMintableUpper_ The upper limit of the maximum number of tokens that can be minted.\n */\n event EditionMaxMintableRangeSet(uint32 editionMaxMintableLower_, uint32 editionMaxMintableUpper_);\n\n /**\n * @dev Emitted when the edition's cutoff time set.\n * @param editionCutoffTime_ The timestamp.\n */\n event EditionCutoffTimeSet(uint32 editionCutoffTime_);\n\n /**\n * @dev Emitted when the `mintRandomnessEnabled` is set.\n * @param mintRandomnessEnabled_ The boolean value.\n */\n event MintRandomnessEnabledSet(bool mintRandomnessEnabled_);\n\n /**\n * @dev Emitted when the `operatorFilteringEnabled` is set.\n * @param operatorFilteringEnabled_ The boolean value.\n */\n event OperatorFilteringEnablededSet(bool operatorFilteringEnabled_);\n\n /**\n * @dev Emitted upon initialization.\n * @param edition_ The address of the edition.\n * @param name_ Name of the collection.\n * @param symbol_ Symbol of the collection.\n * @param metadataModule_ Address of metadata module, address(0x00) if not used.\n * @param baseURI_ Base URI.\n * @param contractURI_ Contract URI for OpenSea storefront.\n * @param fundingRecipient_ Address that receives primary and secondary royalties.\n * @param royaltyBPS_ Royalty amount in bps (basis points).\n * @param editionMaxMintableLower_ The lower bound of the max mintable quantity for the edition.\n * @param editionMaxMintableUpper_ The upper bound of the max mintable quantity for the edition.\n * @param editionCutoffTime_ The timestamp after which `editionMaxMintable` drops from\n * `editionMaxMintableUpper` to\n * `max(_totalMinted(), editionMaxMintableLower)`.\n * @param flags_ The bitwise OR result of the initialization flags.\n * See: {METADATA_IS_FROZEN_FLAG}\n * See: {MINT_RANDOMNESS_ENABLED_FLAG}\n */\n event SoundEditionInitialized(\n address indexed edition_,\n string name_,\n string symbol_,\n address metadataModule_,\n string baseURI_,\n string contractURI_,\n address fundingRecipient_,\n uint16 royaltyBPS_,\n uint32 editionMaxMintableLower_,\n uint32 editionMaxMintableUpper_,\n uint32 editionCutoffTime_,\n uint8 flags_\n );\n\n /**\n * @dev Emitted upon ETH withdrawal.\n * @param recipient The recipient of the withdrawal.\n * @param amount The amount withdrawn.\n * @param caller The account that initiated the withdrawal.\n */\n event ETHWithdrawn(address recipient, uint256 amount, address caller);\n\n /**\n * @dev Emitted upon ERC20 withdrawal.\n * @param recipient The recipient of the withdrawal.\n * @param tokens The addresses of the ERC20 tokens.\n * @param amounts The amount of each token withdrawn.\n * @param caller The account that initiated the withdrawal.\n */\n event ERC20Withdrawn(address recipient, address[] tokens, uint256[] amounts, address caller);\n\n /**\n * @dev Emitted upon a mint.\n * @param to The address to mint to.\n * @param quantity The number of minted.\n * @param fromTokenId The first token ID minted.\n */\n event Minted(address to, uint256 quantity, uint256 fromTokenId);\n\n /**\n * @dev Emitted upon an airdrop.\n * @param to The recipients of the airdrop.\n * @param quantity The number of tokens airdropped to each address in `to`.\n * @param fromTokenId The first token ID minted to the first address in `to`.\n */\n event Airdropped(address[] to, uint256 quantity, uint256 fromTokenId);\n\n /**\n * @dev Emiited when the Sound Automated Market (i.e. bonding curve minter) is set.\n * @param sam_ The Sound Automated Market.\n */\n event SAMSet(address sam_);\n\n // =============================================================\n // ERRORS\n // =============================================================\n\n /**\n * @dev The edition's metadata is frozen (e.g.: `baseURI` can no longer be changed).\n */\n error MetadataIsFrozen();\n\n /**\n * @dev The given `royaltyBPS` is invalid.\n */\n error InvalidRoyaltyBPS();\n\n /**\n * @dev The given `randomnessLockedAfterMinted` value is invalid.\n */\n error InvalidRandomnessLock();\n\n /**\n * @dev The requested quantity exceeds the edition's remaining mintable token quantity.\n * @param available The number of tokens remaining available for mint.\n */\n error ExceedsEditionAvailableSupply(uint32 available);\n\n /**\n * @dev The given amount is invalid.\n */\n error InvalidAmount();\n\n /**\n * @dev The given `fundingRecipient` address is invalid.\n */\n error InvalidFundingRecipient();\n\n /**\n * @dev The `editionMaxMintableLower` must not be greater than `editionMaxMintableUpper`.\n */\n error InvalidEditionMaxMintableRange();\n\n /**\n * @dev The `editionMaxMintable` has already been reached.\n */\n error MaximumHasAlreadyBeenReached();\n\n /**\n * @dev The mint `quantity` cannot exceed `ADDRESS_BATCH_MINT_LIMIT` tokens.\n */\n error ExceedsAddressBatchMintLimit();\n\n /**\n * @dev The mint randomness has already been revealed.\n */\n error MintRandomnessAlreadyRevealed();\n\n /**\n * @dev No addresses to airdrop.\n */\n error NoAddressesToAirdrop();\n\n /**\n * @dev The mint has already concluded.\n */\n error MintHasConcluded();\n\n /**\n * @dev The mint has not concluded.\n */\n error MintNotConcluded();\n\n /**\n * @dev Cannot perform the operation after a token has been minted.\n */\n error MintsAlreadyExist();\n\n /**\n * @dev The token IDs must be in strictly ascending order.\n */\n error TokenIdsNotStrictlyAscending();\n\n // =============================================================\n // PUBLIC / EXTERNAL WRITE FUNCTIONS\n // =============================================================\n\n /**\n * @dev Initializes the contract.\n * @param name_ Name of the collection.\n * @param symbol_ Symbol of the collection.\n * @param metadataModule_ Address of metadata module, address(0x00) if not used.\n * @param baseURI_ Base URI.\n * @param contractURI_ Contract URI for OpenSea storefront.\n * @param fundingRecipient_ Address that receives primary and secondary royalties.\n * @param royaltyBPS_ Royalty amount in bps (basis points).\n * @param editionMaxMintableLower_ The lower bound of the max mintable quantity for the edition.\n * @param editionMaxMintableUpper_ The upper bound of the max mintable quantity for the edition.\n * @param editionCutoffTime_ The timestamp after which `editionMaxMintable` drops from\n * `editionMaxMintableUpper` to\n * `max(_totalMinted(), editionMaxMintableLower)`.\n * @param flags_ The bitwise OR result of the initialization flags.\n * See: {METADATA_IS_FROZEN_FLAG}\n * See: {MINT_RANDOMNESS_ENABLED_FLAG}\n */\n function initialize(\n string memory name_,\n string memory symbol_,\n address metadataModule_,\n string memory baseURI_,\n string memory contractURI_,\n address fundingRecipient_,\n uint16 royaltyBPS_,\n uint32 editionMaxMintableLower_,\n uint32 editionMaxMintableUpper_,\n uint32 editionCutoffTime_,\n uint8 flags_\n ) external;\n\n /**\n * @dev Mints `quantity` tokens to addrress `to`\n * Each token will be assigned a token ID that is consecutively increasing.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have either the\n * `ADMIN_ROLE`, `MINTER_ROLE`, which can be granted via {grantRole}.\n * Multiple minters, such as different minter contracts,\n * can be authorized simultaneously.\n *\n * @param to Address to mint to.\n * @param quantity Number of tokens to mint.\n * @return fromTokenId The first token ID minted.\n */\n function mint(address to, uint256 quantity) external payable returns (uint256 fromTokenId);\n\n /**\n * @dev Mints `quantity` tokens to each of the addresses in `to`.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the\n * `ADMIN_ROLE`, which can be granted via {grantRole}.\n *\n * @param to Address to mint to.\n * @param quantity Number of tokens to mint.\n * @return fromTokenId The first token ID minted.\n */\n function airdrop(address[] calldata to, uint256 quantity) external returns (uint256 fromTokenId);\n\n /**\n * @dev Mints `quantity` tokens to addrress `to`\n * Each token will be assigned a token ID that is consecutively increasing.\n *\n * Calling conditions:\n * - The caller must be the bonding curve contract.\n *\n * @param to Address to mint to.\n * @param quantity Number of tokens to mint.\n * @return fromTokenId The first token ID minted.\n */\n function samMint(address to, uint256 quantity) external payable returns (uint256 fromTokenId);\n\n /**\n * @dev Burns the `tokenIds`.\n *\n * Calling conditions:\n * - The caller must be the bonding curve contract.\n *\n * @param burner The initiator of the burn.\n * @param tokenIds The list of token IDs to burn.\n */\n function samBurn(address burner, uint256[] calldata tokenIds) external;\n\n /**\n * @dev Withdraws collected ETH royalties to the fundingRecipient.\n */\n function withdrawETH() external;\n\n /**\n * @dev Withdraws collected ERC20 royalties to the fundingRecipient.\n * @param tokens array of ERC20 tokens to withdraw\n */\n function withdrawERC20(address[] calldata tokens) external;\n\n /**\n * @dev Sets metadata module.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param metadataModule Address of metadata module.\n */\n function setMetadataModule(address metadataModule) external;\n\n /**\n * @dev Sets global base URI.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param baseURI The base URI to be set.\n */\n function setBaseURI(string memory baseURI) external;\n\n /**\n * @dev Sets contract URI.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param contractURI The contract URI to be set.\n */\n function setContractURI(string memory contractURI) external;\n\n /**\n * @dev Freezes metadata by preventing any more changes to base URI.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n */\n function freezeMetadata() external;\n\n /**\n * @dev Sets funding recipient address.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param fundingRecipient Address to be set as the new funding recipient.\n */\n function setFundingRecipient(address fundingRecipient) external;\n\n /**\n * @dev Sets royalty amount in bps (basis points).\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param bps The new royalty basis points to be set.\n */\n function setRoyalty(uint16 bps) external;\n\n /**\n * @dev Sets the edition max mintable range.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param editionMaxMintableLower_ The lower limit of the maximum number of tokens that can be minted.\n * @param editionMaxMintableUpper_ The upper limit of the maximum number of tokens that can be minted.\n */\n function setEditionMaxMintableRange(uint32 editionMaxMintableLower_, uint32 editionMaxMintableUpper_) external;\n\n /**\n * @dev Sets the timestamp after which, the `editionMaxMintable` drops\n * from `editionMaxMintableUpper` to `editionMaxMintableLower.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param editionCutoffTime_ The timestamp.\n */\n function setEditionCutoffTime(uint32 editionCutoffTime_) external;\n\n /**\n * @dev Sets whether the `mintRandomness` is enabled.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param mintRandomnessEnabled_ The boolean value.\n */\n function setMintRandomnessEnabled(bool mintRandomnessEnabled_) external;\n\n /**\n * @dev Sets whether OpenSea operator filtering is enabled.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param operatorFilteringEnabled_ The boolean value.\n */\n function setOperatorFilteringEnabled(bool operatorFilteringEnabled_) external;\n\n /**\n * @dev Sets the Sound Automated Market (i.e. bonding curve minter).\n *\n * Calling conditions:\n * - The caller must be the owner of the contract, or have the `ADMIN_ROLE`.\n *\n * @param sam_ The Sound Automated Market.\n */\n function setSAM(address sam_) external;\n\n // =============================================================\n // PUBLIC / EXTERNAL VIEW FUNCTIONS\n // =============================================================\n\n /**\n * @dev Returns the edition info.\n * @return editionInfo The latest value.\n */\n function editionInfo() external view returns (EditionInfo memory editionInfo);\n\n /**\n * @dev Returns the minter role flag.\n * @return The constant value.\n */\n function MINTER_ROLE() external view returns (uint256);\n\n /**\n * @dev Returns the admin role flag.\n * @return The constant value.\n */\n function ADMIN_ROLE() external view returns (uint256);\n\n /**\n * @dev Returns the maximum limit for the mint or airdrop `quantity`.\n * Prevents the first-time transfer costs for tokens near the end of large mint batches\n * via ERC721A from becoming too expensive due to the need to scan many storage slots.\n * See: https://chiru-labs.github.io/ERC721A/#/tips?id=batch-size\n * @return The constant value.\n */\n function ADDRESS_BATCH_MINT_LIMIT() external pure returns (uint256);\n\n /**\n * @dev Returns the bit flag to freeze the metadata on initialization.\n * @return The constant value.\n */\n function METADATA_IS_FROZEN_FLAG() external pure returns (uint8);\n\n /**\n * @dev Returns the bit flag to enable the mint randomness feature on initialization.\n * @return The constant value.\n */\n function MINT_RANDOMNESS_ENABLED_FLAG() external pure returns (uint8);\n\n /**\n * @dev Returns the bit flag to enable OpenSea operator filtering.\n * @return The constant value.\n */\n function OPERATOR_FILTERING_ENABLED_FLAG() external pure returns (uint8);\n\n /**\n * @dev Returns the base token URI for the collection.\n * @return The configured value.\n */\n function baseURI() external view returns (string memory);\n\n /**\n * @dev Returns the contract URI to be used by Opensea.\n * See: https://docs.opensea.io/docs/contract-level-metadata\n * @return The configured value.\n */\n function contractURI() external view returns (string memory);\n\n /**\n * @dev Returns the address of the funding recipient.\n * @return The configured value.\n */\n function fundingRecipient() external view returns (address);\n\n /**\n * @dev Returns the maximum amount of tokens mintable for this edition.\n * @return The configured value.\n */\n function editionMaxMintable() external view returns (uint32);\n\n /**\n * @dev Returns the upper bound for the maximum tokens that can be minted for this edition.\n * @return The configured value.\n */\n function editionMaxMintableUpper() external view returns (uint32);\n\n /**\n * @dev Returns the lower bound for the maximum tokens that can be minted for this edition.\n * @return The configured value.\n */\n function editionMaxMintableLower() external view returns (uint32);\n\n /**\n * @dev Returns the timestamp after which `editionMaxMintable` drops from\n * `editionMaxMintableUpper` to `editionMaxMintableLower`.\n * @return The configured value.\n */\n function editionCutoffTime() external view returns (uint32);\n\n /**\n * @dev Returns the address of the metadata module.\n * @return The configured value.\n */\n function metadataModule() external view returns (address);\n\n /**\n * @dev Returns the randomness based on latest block hash, which is stored upon each mint.\n * unless {mintConcluded} is true.\n * Used for game mechanics like the Sound Golden Egg.\n * Returns 0 before revealed.\n * WARNING: This value should NOT be used for any reward of significant monetary\n * value, due to it being computed via a purely on-chain psuedorandom mechanism.\n * @return The latest value.\n */\n function mintRandomness() external view returns (uint256);\n\n /**\n * @dev Returns whether the `mintRandomness` has been enabled.\n * @return The configured value.\n */\n function mintRandomnessEnabled() external view returns (bool);\n\n /**\n * @dev Returns whether the `operatorFilteringEnabled` has been enabled.\n * @return The configured value.\n */\n function operatorFilteringEnabled() external view returns (bool);\n\n /**\n * @dev Returns whether the mint has been concluded.\n * @return The latest value.\n */\n function mintConcluded() external view returns (bool);\n\n /**\n * @dev Returns the royalty basis points.\n * @return The configured value.\n */\n function royaltyBPS() external view returns (uint16);\n\n /**\n * @dev Returns whether the metadata module is frozen.\n * @return The configured value.\n */\n function isMetadataFrozen() external view returns (bool);\n\n /**\n * @dev Returns the sound automated market, if any.\n * @return The configured value.\n */\n function sam() external view returns (address);\n\n /**\n * @dev Returns the next token ID to be minted.\n * @return The latest value.\n */\n function nextTokenId() external view returns (uint256);\n\n /**\n * @dev Returns the number of tokens minted by `owner`.\n * @param owner Address to query for number minted.\n * @return The latest value.\n */\n function numberMinted(address owner) external view returns (uint256);\n\n /**\n * @dev Returns the number of tokens burned by `owner`.\n * @param owner Address to query for number burned.\n * @return The latest value.\n */\n function numberBurned(address owner) external view returns (uint256);\n\n /**\n * @dev Returns the total amount of tokens minted.\n * @return The latest value.\n */\n function totalMinted() external view returns (uint256);\n\n /**\n * @dev Returns the total amount of tokens burned.\n * @return The latest value.\n */\n function totalBurned() external view returns (uint256);\n\n /**\n * @dev Informs other contracts which interfaces this contract supports.\n * Required by https://eips.ethereum.org/EIPS/eip-165\n * @param interfaceId The interface id to check.\n * @return Whether the `interfaceId` is supported.\n */\n function supportsInterface(bytes4 interfaceId)\n external\n view\n override(IERC721AUpgradeable, IERC165Upgradeable)\n returns (bool);\n}\n"},"contracts/modules/SAM.sol":{"content":"// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.16;\n\nimport { Ownable, OwnableRoles } from \"solady/auth/OwnableRoles.sol\";\nimport { MerkleProofLib } from \"solady/utils/MerkleProofLib.sol\";\nimport { SafeCastLib } from \"solady/utils/SafeCastLib.sol\";\nimport { SafeTransferLib } from \"solady/utils/SafeTransferLib.sol\";\nimport { IERC165 } from \"openzeppelin/utils/introspection/IERC165.sol\";\nimport { ISAM, SAMInfo } from \"./interfaces/ISAM.sol\";\nimport { BondingCurveLib } from \"./utils/BondingCurveLib.sol\";\nimport { ISoundEditionV1_2 } from \"@core/interfaces/ISoundEditionV1_2.sol\";\n\n/*\n * @title SAM\n * @notice Module for Sound automated market.\n * @author Sound.xyz\n */\ncontract SAM is ISAM, Ownable {\n // =============================================================\n // CONSTANTS\n // =============================================================\n\n /**\n * @dev This is the denominator, in basis points (BPS), for any of the fees.\n */\n uint16 public constant BPS_DENOMINATOR = 10_000;\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the platform fees.\n */\n uint16 public constant MAX_PLATFORM_FEE_BPS = 500;\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the artist fees.\n */\n uint16 public constant MAX_ARTIST_FEE_BPS = 1_000;\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the affiliate fees.\n */\n uint16 public constant MAX_AFFILIATE_FEE_BPS = 500;\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the golden egg fees.\n */\n uint16 public constant MAX_GOLDEN_EGG_FEE_BPS = 500;\n\n // =============================================================\n // STORAGE\n // =============================================================\n\n /**\n * @dev How much platform fees have been accrued.\n */\n uint128 public platformFeesAccrued;\n\n /**\n * @dev The platform fee in basis points.\n */\n uint16 public platformFeeBPS;\n\n /**\n * @dev Just in case. Won't cost much overhead anyway since it is packed.\n */\n bool internal _reentrancyGuard;\n\n /**\n * @dev The platform fee address.\n */\n address public platformFeeAddress;\n\n /**\n * @dev The data for the sound automated markets.\n * edition => SAMData\n */\n mapping(address => SAMData) internal _samData;\n\n /**\n * @dev Maps an address to how much affiliate fees have they accrued.\n */\n mapping(address => uint128) internal _affiliateFeesAccrued;\n\n // =============================================================\n // CONSTRUCTOR\n // =============================================================\n\n constructor() payable {\n _initializeOwner(msg.sender);\n }\n\n // =============================================================\n // PUBLIC / EXTERNAL WRITE FUNCTIONS\n // =============================================================\n\n /**\n * @inheritdoc ISAM\n */\n function create(\n address edition,\n uint96 basePrice,\n uint96 inflectionPrice,\n uint32 inflectionPoint,\n uint16 artistFeeBPS,\n uint16 goldenEggFeeBPS,\n uint16 affiliateFeeBPS\n ) public onlyEditionOwnerOrAdmin(edition) onlyBeforeMintConcluded(edition) {\n // We don't use modifiers here in order to prevent stack too deep.\n if (inflectionPrice == 0) revert InflectionPriceIsZero();\n if (inflectionPoint == 0) revert InflectionPointIsZero();\n if (artistFeeBPS > MAX_ARTIST_FEE_BPS) revert InvalidArtistFeeBPS();\n if (goldenEggFeeBPS > MAX_GOLDEN_EGG_FEE_BPS) revert InvalidGoldenEggFeeBPS();\n if (affiliateFeeBPS > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS();\n\n SAMData storage data = _samData[edition];\n\n if (data.inflectionPrice != 0) revert SAMAlreadyExists();\n\n data.basePrice = basePrice;\n data.inflectionPrice = inflectionPrice;\n data.inflectionPoint = inflectionPoint;\n data.artistFeeBPS = artistFeeBPS;\n data.goldenEggFeeBPS = goldenEggFeeBPS;\n data.affiliateFeeBPS = affiliateFeeBPS;\n\n emit Created(\n edition,\n basePrice,\n inflectionPrice,\n inflectionPoint,\n artistFeeBPS,\n goldenEggFeeBPS,\n affiliateFeeBPS\n );\n }\n\n /**\n * For avoiding stack too deep.\n */\n struct _BuyTemps {\n uint256 fromCurveSupply;\n uint256 fromTokenId;\n uint256 requiredEtherValue;\n uint256 feePerBPS;\n uint256 platformFee;\n uint256 artistFee;\n uint256 goldenEggFee;\n uint256 affiliateFee;\n bool affiliated;\n }\n\n /**\n * @inheritdoc ISAM\n */\n function buy(\n address edition,\n address to,\n uint32 quantity,\n address affiliate,\n bytes32[] calldata affiliateProof\n ) public payable nonReentrant {\n _BuyTemps memory t;\n SAMData storage data = _getSAMData(edition);\n t.fromCurveSupply = data.supply; // Cache the `data.supply`.\n\n (t.requiredEtherValue, t.feePerBPS) = _totalBuyPriceAndFeePerBPS(data, uint32(t.fromCurveSupply), quantity);\n\n if (msg.value < t.requiredEtherValue) revert Underpaid(msg.value, t.requiredEtherValue);\n\n if (data.buyFrozen) revert BuyIsFrozen();\n\n unchecked {\n // Compute the artist fee.\n t.artistFee = t.feePerBPS * uint256(data.artistFeeBPS);\n\n // Compute the platform fee.\n t.platformFee = t.feePerBPS * uint256(platformFeeBPS);\n // Accrue the platform fee.\n platformFeesAccrued = SafeCastLib.toUint128(uint256(platformFeesAccrued) + t.platformFee);\n\n // Check if the affiliate is actually affiliated for edition with the affiliate proof.\n t.affiliated = isAffiliatedWithProof(edition, affiliate, affiliateProof);\n // If affiliated, compute and accrue the affiliate fee.\n if (t.affiliated) {\n // Compute the affiliate fee.\n t.affiliateFee = t.feePerBPS * uint256(data.affiliateFeeBPS);\n // Accrue the affiliate fee.\n _affiliateFeesAccrued[affiliate] = SafeCastLib.toUint128(\n uint256(_affiliateFeesAccrued[affiliate]) + t.affiliateFee\n );\n } else {\n // Otherwise, add the affiliate fee to the artist fee instead.\n t.artistFee += t.feePerBPS * uint256(data.affiliateFeeBPS);\n }\n\n // Mint the tokens and transfer the artist fee to the edition contract.\n // Will revert if the `quantity` is zero.\n t.fromTokenId = ISoundEditionV1_2(edition).samMint{ value: t.artistFee }(to, quantity);\n\n // Compute the golden egg fee.\n t.goldenEggFee = t.feePerBPS * uint256(data.goldenEggFeeBPS);\n // Accrue the golden egg fee.\n data.goldenEggFeesAccrued = SafeCastLib.toUint128(uint256(data.goldenEggFeesAccrued) + t.goldenEggFee);\n\n // Add `quantity` to the supply.\n // We add a safecast here just in case.\n data.supply = SafeCastLib.toUint32(t.fromCurveSupply + uint256(quantity));\n\n // Refund any excess ETH.\n if (msg.value > t.requiredEtherValue) {\n SafeTransferLib.forceSafeTransferETH(msg.sender, msg.value - t.requiredEtherValue);\n }\n\n emit Bought(\n edition,\n to,\n t.fromTokenId,\n uint32(t.fromCurveSupply),\n quantity,\n uint128(t.requiredEtherValue),\n uint128(t.platformFee),\n uint128(t.artistFee),\n uint128(t.goldenEggFee),\n uint128(t.affiliateFee),\n affiliate,\n t.affiliated\n );\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function sell(\n address edition,\n uint256[] calldata tokenIds,\n uint256 minimumPayout,\n address payoutTo\n ) public nonReentrant {\n unchecked {\n SAMData storage data = _getSAMData(edition);\n\n uint256 quantity = tokenIds.length;\n\n uint256 initialSupply = data.supply;\n\n // The `_totalSellPrice` function will revert with\n // `InsufficientSupply(available = initialSupply, required = quantity)`\n // if `initialSupply < quantity`.\n uint256 payout = _totalSellPrice(data, uint32(initialSupply), 0, uint32(quantity));\n\n if (payout < minimumPayout) revert InsufficientPayout(payout, minimumPayout);\n\n ISoundEditionV1_2(edition).samBurn(msg.sender, tokenIds);\n\n // Decrease the supply. The `_totalSellPrice` function call above\n // has already check that `initialSupply >= quantity`.\n data.supply = uint32(initialSupply - quantity);\n\n SafeTransferLib.forceSafeTransferETH(payoutTo, payout);\n\n emit Sold(edition, payoutTo, uint32(initialSupply), tokenIds, uint128(payout));\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setBasePrice(address edition, uint96 basePrice)\n public\n onlyEditionOwnerOrAdmin(edition)\n onlyBeforeMintConcluded(edition)\n {\n SAMData storage data = _getSAMData(edition);\n data.basePrice = basePrice;\n emit BasePriceSet(edition, basePrice);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setInflectionPrice(address edition, uint96 inflectionPrice)\n public\n onlyEditionOwnerOrAdmin(edition)\n onlyBeforeMintConcluded(edition)\n {\n SAMData storage data = _getSAMData(edition);\n if (inflectionPrice == 0) revert InflectionPriceIsZero();\n data.inflectionPrice = inflectionPrice;\n emit InflectionPriceSet(edition, inflectionPrice);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setInflectionPoint(address edition, uint32 inflectionPoint)\n public\n onlyEditionOwnerOrAdmin(edition)\n onlyBeforeMintConcluded(edition)\n {\n SAMData storage data = _getSAMData(edition);\n if (inflectionPoint == 0) revert InflectionPointIsZero();\n data.inflectionPoint = inflectionPoint;\n emit InflectionPointSet(edition, inflectionPoint);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setArtistFee(address edition, uint16 bps)\n public\n onlyEditionOwnerOrAdmin(edition)\n onlyBeforeMintConcluded(edition)\n {\n SAMData storage data = _getSAMData(edition);\n if (bps > MAX_ARTIST_FEE_BPS) revert InvalidArtistFeeBPS();\n data.artistFeeBPS = bps;\n emit ArtistFeeSet(edition, bps);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setGoldenEggFee(address edition, uint16 bps)\n public\n onlyEditionOwnerOrAdmin(edition)\n onlyBeforeMintConcluded(edition)\n {\n SAMData storage data = _getSAMData(edition);\n if (bps > MAX_GOLDEN_EGG_FEE_BPS) revert InvalidGoldenEggFeeBPS();\n data.goldenEggFeeBPS = bps;\n emit GoldenEggFeeSet(edition, bps);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setAffiliateFee(address edition, uint16 bps)\n public\n onlyEditionOwnerOrAdmin(edition)\n onlyBeforeMintConcluded(edition)\n {\n SAMData storage data = _getSAMData(edition);\n if (bps > MAX_AFFILIATE_FEE_BPS) revert InvalidAffiliateFeeBPS();\n data.affiliateFeeBPS = bps;\n emit AffiliateFeeSet(edition, bps);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setAffiliateMerkleRoot(address edition, bytes32 root) public onlyEditionOwnerOrAdmin(edition) {\n // Note that we want to allow adding a root even while the bonding curve\n // is still ongoing, in case the need to prevent spam arises.\n\n SAMData storage data = _getSAMData(edition);\n data.affiliateMerkleRoot = root;\n emit AffiliateMerkleRootSet(edition, root);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function freezeBuy(address edition) public onlyEditionOwnerOrAdmin(edition) {\n SAMData storage data = _getSAMData(edition);\n if (data.buyFrozen) revert BuyIsFrozen();\n data.buyFrozen = true;\n emit BuyFrozen(edition);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function withdrawForAffiliate(address affiliate) public nonReentrant {\n uint128 accrued = _affiliateFeesAccrued[affiliate];\n if (accrued != 0) {\n _affiliateFeesAccrued[affiliate] = 0;\n SafeTransferLib.forceSafeTransferETH(affiliate, accrued);\n emit AffiliateFeesWithdrawn(affiliate, accrued);\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function withdrawForPlatform() public nonReentrant {\n address to = platformFeeAddress;\n if (to == address(0)) revert PlatformFeeAddressIsZero();\n uint128 accrued = platformFeesAccrued;\n if (accrued != 0) {\n platformFeesAccrued = 0;\n SafeTransferLib.forceSafeTransferETH(to, accrued);\n emit PlatformFeesWithdrawn(accrued);\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function withdrawForGoldenEgg(address edition) public nonReentrant {\n SAMData storage data = _getSAMData(edition);\n uint128 accrued = data.goldenEggFeesAccrued;\n if (accrued != 0) {\n data.goldenEggFeesAccrued = 0;\n address receipient = goldenEggFeeRecipient(edition);\n SafeTransferLib.forceSafeTransferETH(receipient, accrued);\n emit GoldenEggFeesWithdrawn(edition, receipient, accrued);\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setPlatformFee(uint16 bps) public onlyOwner {\n if (bps > MAX_PLATFORM_FEE_BPS) revert InvalidPlatformFeeBPS();\n platformFeeBPS = bps;\n emit PlatformFeeSet(bps);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function setPlatformFeeAddress(address addr) public onlyOwner {\n if (addr == address(0)) revert PlatformFeeAddressIsZero();\n platformFeeAddress = addr;\n emit PlatformFeeAddressSet(addr);\n }\n\n // =============================================================\n // PUBLIC / EXTERNAL VIEW FUNCTIONS\n // =============================================================\n\n /**\n * @inheritdoc ISAM\n */\n function samInfo(address edition) external view returns (SAMInfo memory info) {\n SAMData storage data = _getSAMData(edition);\n info.basePrice = data.basePrice;\n info.inflectionPrice = data.inflectionPrice;\n info.inflectionPoint = data.inflectionPoint;\n info.goldenEggFeesAccrued = data.goldenEggFeesAccrued;\n info.supply = data.supply;\n info.artistFeeBPS = data.artistFeeBPS;\n info.affiliateFeeBPS = data.affiliateFeeBPS;\n info.goldenEggFeeBPS = data.goldenEggFeeBPS;\n }\n\n /**\n * @inheritdoc ISAM\n */\n function totalValue(\n address edition,\n uint32 fromSupply,\n uint32 quantity\n ) public view returns (uint256 total) {\n unchecked {\n SAMData storage data = _getSAMData(edition);\n total = uint256(data.basePrice) * uint256(quantity);\n total += BondingCurveLib.sigmoid2Sum(data.inflectionPoint, data.inflectionPrice, fromSupply, quantity);\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function totalBuyPrice(\n address edition,\n uint32 supplyForwardOffset,\n uint32 quantity\n ) public view returns (uint256 total) {\n unchecked {\n SAMData storage data = _getSAMData(edition);\n uint256 fromSupply = uint256(data.supply) + uint256(supplyForwardOffset);\n (total, ) = _totalBuyPriceAndFeePerBPS(data, SafeCastLib.toUint32(fromSupply), quantity);\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function totalSellPrice(\n address edition,\n uint32 supplyBackwardOffset,\n uint32 quantity\n ) public view returns (uint256 total) {\n SAMData storage data = _getSAMData(edition);\n\n // The `_totalSellPrice` function will revert with\n // `InsufficientSupply(available = data.supply, required = supplyBackwardOffset + quantity)`\n // if `data.supply < supplyBackwardOffset + quantity`.\n total = _totalSellPrice(data, data.supply, supplyBackwardOffset, quantity);\n }\n\n /**\n * @inheritdoc ISAM\n */\n function goldenEggFeeRecipient(address edition) public view returns (address receipient) {\n // We use assembly because we don't want to revert\n // if the `metadataModule` is not a valid metadata module contract.\n // Plain solidity requires an extra codesize check.\n assembly {\n // Initialize the receipient to the edition by default.\n receipient := edition\n // Store the function selector of `metadataModule()`.\n mstore(0x00, 0x3684d100)\n\n if iszero(and(eq(returndatasize(), 0x20), staticcall(gas(), edition, 0x1c, 0x04, 0x00, 0x20))) {\n // For better gas estimation, and to require that edition\n // is a contract with the `metadataModule()` function.\n revert(0, 0)\n }\n\n let metadataModule := mload(0x00)\n // Store the function selector of `getGoldenEggTokenId(address)`.\n mstore(0x00, 0x4baca2b5)\n mstore(0x20, edition)\n\n if iszero(staticcall(gas(), metadataModule, 0x1c, 0x24, 0x20, 0x20)) {\n // If there is no returndata upon revert,\n // it is likely due to an out-of-gas error.\n if iszero(returndatasize()) {\n revert(0, 0) // For better gas estimation.\n }\n }\n\n if eq(returndatasize(), 0x20) {\n // Store the function selector of `ownerOf(uint256)`.\n mstore(0x00, 0x6352211e)\n // The `goldenEggTokenId` is already in slot 0x20,\n // as the previous staticcall directly writes the output to slot 0x20.\n\n if iszero(staticcall(gas(), edition, 0x1c, 0x24, 0x00, 0x20)) {\n // If there is no returndata upon revert,\n // it is likely due to an out-of-gas error.\n if iszero(returndatasize()) {\n revert(0, 0) // For better gas estimation.\n }\n }\n\n if eq(returndatasize(), 0x20) {\n receipient := mload(0x00)\n }\n }\n }\n }\n\n /**\n * @inheritdoc ISAM\n */\n function goldenEggFeesAccrued(address edition) public view returns (uint128) {\n return _getSAMData(edition).goldenEggFeesAccrued;\n }\n\n /**\n * @inheritdoc ISAM\n */\n function affiliateFeesAccrued(address affiliate) public view returns (uint128) {\n return _affiliateFeesAccrued[affiliate];\n }\n\n /**\n * @inheritdoc ISAM\n */\n function isAffiliatedWithProof(\n address edition,\n address affiliate,\n bytes32[] calldata affiliateProof\n ) public view returns (bool) {\n bytes32 root = _getSAMData(edition).affiliateMerkleRoot;\n // If the root is empty, then use the default logic.\n if (root == bytes32(0)) {\n return affiliate != address(0);\n }\n // Otherwise, check if the affiliate is in the Merkle tree.\n return MerkleProofLib.verify(affiliateProof, root, keccak256(abi.encodePacked(affiliate)));\n }\n\n /**\n * @inheritdoc ISAM\n */\n function isAffiliated(address edition, address affiliate) public view returns (bool) {\n return isAffiliatedWithProof(edition, affiliate, MerkleProofLib.emptyProof());\n }\n\n /**\n * @inheritdoc ISAM\n */\n function affiliateMerkleRoot(address edition) external view returns (bytes32) {\n return _getSAMData(edition).affiliateMerkleRoot;\n }\n\n /**\n * @inheritdoc IERC165\n */\n function supportsInterface(bytes4 interfaceId) public pure override(IERC165) returns (bool) {\n return interfaceId == this.supportsInterface.selector || interfaceId == type(ISAM).interfaceId;\n }\n\n /**\n * @inheritdoc ISAM\n */\n function moduleInterfaceId() public pure returns (bytes4) {\n return type(ISAM).interfaceId;\n }\n\n // =============================================================\n // INTERNAL / PRIVATE HELPERS\n // =============================================================\n\n /**\n * @dev Restricts the function to be only callable by the owner or admin of `edition`.\n * @param edition The edition address.\n */\n modifier onlyEditionOwnerOrAdmin(address edition) {\n if (msg.sender != OwnableRoles(edition).owner())\n if (!OwnableRoles(edition).hasAnyRole(msg.sender, ISoundEditionV1_2(edition).ADMIN_ROLE()))\n revert Unauthorized();\n _;\n }\n\n /**\n * @dev Requires that the `edition` does not have any tokens minted.\n * @param edition The edition address.\n */\n modifier onlyBeforeMintConcluded(address edition) {\n if (ISoundEditionV1_2(edition).mintConcluded()) revert MintHasConcluded();\n _;\n }\n\n /**\n * @dev Guards the function from reentrancy.\n */\n modifier nonReentrant() {\n require(_reentrancyGuard == false);\n _reentrancyGuard = true;\n _;\n _reentrancyGuard = false;\n }\n\n /**\n * @dev Returns the storage pointer to the SAMData for `edition`.\n * Reverts if the Sound Automated Market does not exist.\n * @param edition The edition address.\n * @return data Storage pointer to a SAMData.\n */\n function _getSAMData(address edition) internal view returns (SAMData storage data) {\n data = _samData[edition];\n if (data.inflectionPrice == 0) revert SAMDoesNotExist();\n }\n\n /**\n * @dev Returns the total buy price and the fee per BPS.\n * @param data Storage pointer to a SAMData.\n * @param fromSupply The starting SAM supply.\n * @param quantity The number of tokens to be minted.\n * @return total The total buy price.\n * @return feePerBPS The amount of fee per BPS.\n */\n function _totalBuyPriceAndFeePerBPS(\n SAMData storage data,\n uint32 fromSupply,\n uint32 quantity\n ) internal view returns (uint256 total, uint256 feePerBPS) {\n unchecked {\n total = uint256(data.basePrice) * uint256(quantity);\n total += BondingCurveLib.sigmoid2Sum(data.inflectionPoint, data.inflectionPrice, fromSupply, quantity);\n uint256 totalFeeBPS = platformFeeBPS;\n totalFeeBPS += data.artistFeeBPS;\n totalFeeBPS += data.affiliateFeeBPS;\n totalFeeBPS += data.goldenEggFeeBPS;\n (total, feePerBPS) = BondingCurveLib.totalBuyPriceAndFeePerBPS(total, totalFeeBPS, BPS_DENOMINATOR);\n }\n }\n\n /**\n * @dev Returns the total sell price.\n * @param data Storage pointer to a SAMData.\n * @param supply The current number of tokens.\n * @param supplyBackwardOffset The backward offset of the supply.\n * @param quantity The number of tokens to be minted.\n * @return total The total buy price.\n */\n function _totalSellPrice(\n SAMData storage data,\n uint32 supply,\n uint32 supplyBackwardOffset,\n uint32 quantity\n ) internal view returns (uint256 total) {\n unchecked {\n uint256 offset = uint256(supplyBackwardOffset) + uint256(quantity);\n // Revert with `InsufficientSupply(available = supply, required = offset)`\n // if `supply < offset`.\n if (supply < offset) revert InsufficientSupply(supply, offset);\n uint32 fromSupply = uint32(uint256(supply) - offset);\n total = uint256(data.basePrice) * uint256(quantity);\n total += BondingCurveLib.sigmoid2Sum(data.inflectionPoint, data.inflectionPrice, fromSupply, quantity);\n }\n }\n}\n"},"contracts/modules/interfaces/ISAM.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.16;\n\nimport { IERC165 } from \"openzeppelin/utils/introspection/IERC165.sol\";\n\n/**\n * @dev Data unique to a Sound Automated Market (i.e. bonding curve mint).\n */\nstruct SAMInfo {\n uint96 basePrice;\n uint96 inflectionPrice;\n uint32 inflectionPoint;\n uint128 goldenEggFeesAccrued;\n uint32 supply;\n uint16 artistFeeBPS;\n uint16 affiliateFeeBPS;\n uint16 goldenEggFeeBPS;\n}\n\n/**\n * @title ISAM\n * @dev Interface for the Sound Automated Market module.\n * @author Sound.xyz\n */\ninterface ISAM is IERC165 {\n // =============================================================\n // STRUCTS\n // =============================================================\n struct SAMData {\n // The price added to the bonding curve price.\n uint96 basePrice;\n // The sigmoid inflection price of the bonding curve.\n uint96 inflectionPrice;\n // The sigmoid inflection point of the bonding curve.\n uint32 inflectionPoint;\n // The amount of fees accrued by the golden egg.\n uint128 goldenEggFeesAccrued;\n // The amount of tokens in the bonding curve.\n uint32 supply;\n // The fee BPS (basis points) to pay the artist.\n uint16 artistFeeBPS;\n // The fee BPS (basis points) to pay affiliates.\n uint16 affiliateFeeBPS;\n // The fee BPS (basis points) to pay the golden egg holder.\n uint16 goldenEggFeeBPS;\n // The buy is forever closed.\n bool buyFrozen;\n // The affiliate Merkle root, if any.\n bytes32 affiliateMerkleRoot;\n }\n\n // =============================================================\n // EVENTS\n // =============================================================\n\n /**\n * @dev Emitted when a bonding curve is created.\n * @param edition The edition address.\n * @param inflectionPrice The sigmoid inflection price of the bonding curve.\n * @param inflectionPoint The sigmoid inflection point of the bonding curve.\n * @param artistFeeBPS The fee BPS (basis points) to pay the artist.\n * @param goldenEggFeeBPS The fee BPS (basis points) to pay the golden egg holder.\n * @param affiliateFeeBPS The fee BPS (basis points) to pay affiliates.\n */\n event Created(\n address indexed edition,\n uint96 basePrice,\n uint96 inflectionPrice,\n uint32 inflectionPoint,\n uint16 artistFeeBPS,\n uint16 goldenEggFeeBPS,\n uint16 affiliateFeeBPS\n );\n\n /**\n * @dev Emitted when tokens are bought from the bonding curve.\n * @param edition The edition address.\n * @param buyer Address of the buyer.\n * @param fromTokenId The starting token ID minted for the batch.\n * @param fromCurveSupply The start of the curve supply for the batch.\n * @param quantity The number of tokens bought.\n * @param totalPayment The total amount of ETH paid.\n * @param platformFee The cut paid to the platform.\n * @param artistFee The cut paid to the artist.\n * @param goldenEggFee The cut paid to the golden egg.\n * @param affiliateFee The cut paid to the affiliate.\n * @param affiliate The affiliate's address.\n * @param affiliated Whether the affiliate is affiliated.\n */\n event Bought(\n address indexed edition,\n address indexed buyer,\n uint256 fromTokenId,\n uint32 fromCurveSupply,\n uint32 quantity,\n uint128 totalPayment,\n uint128 platformFee,\n uint128 artistFee,\n uint128 goldenEggFee,\n uint128 affiliateFee,\n address affiliate,\n bool affiliated\n );\n\n /**\n * @dev Emitted when tokens are sold into the bonding curve.\n * @param edition The edition address.\n * @param seller Address of the seller.\n * @param fromCurveSupply The start of the curve supply for the batch.\n * @param tokenIds The token IDs burned.\n * @param totalPayout The total amount of ETH paid out.\n */\n event Sold(\n address indexed edition,\n address indexed seller,\n uint32 fromCurveSupply,\n uint256[] tokenIds,\n uint128 totalPayout\n );\n\n /**\n * @dev Emitted when the `basePrice` is updated.\n * @param edition The edition address.\n * @param basePrice The price added to the bonding curve price.\n */\n event BasePriceSet(address indexed edition, uint96 basePrice);\n\n /**\n * @dev Emitted when the `inflectionPrice` is updated.\n * @param edition The edition address.\n * @param inflectionPrice The sigmoid inflection price of the bonding curve.\n */\n event InflectionPriceSet(address indexed edition, uint96 inflectionPrice);\n\n /**\n * @dev Emitted when the `inflectionPoint` is updated.\n * @param edition The edition address.\n * @param inflectionPoint The sigmoid inflection point of the bonding curve.\n */\n event InflectionPointSet(address indexed edition, uint32 inflectionPoint);\n\n /**\n * @dev Emitted when the `artistFeeBPS` is updated.\n * @param edition The edition address.\n * @param bps The affiliate fee basis points.\n */\n event ArtistFeeSet(address indexed edition, uint16 bps);\n\n /**\n * @dev Emitted when the `affiliateFeeBPS` is updated.\n * @param edition The edition address.\n * @param bps The affiliate fee basis points.\n */\n event AffiliateFeeSet(address indexed edition, uint16 bps);\n\n /**\n * @dev Emitted when the Merkle root for an affiliate allow list is updated.\n * @param edition The edition address.\n * @param root The Merkle root for the affiliate allow list.\n */\n event AffiliateMerkleRootSet(address indexed edition, bytes32 root);\n\n /**\n * @dev Emitted when the `goldenEggFeeBPS` is updated.\n * @param edition The edition address.\n * @param bps The golden egg fee basis points.\n */\n event GoldenEggFeeSet(address indexed edition, uint16 bps);\n\n /**\n * @dev Emitted when buy for `edition` is closed permanently.\n * @param edition The edition address.\n */\n event BuyFrozen(address indexed edition);\n\n /**\n * @dev Emitted when the `platformFeeBPS` is updated.\n * @param bps The platform fee basis points.\n */\n event PlatformFeeSet(uint16 bps);\n\n /**\n * @dev Emitted when the `platformFeeAddress` is updated.\n * @param addr The platform fee address.\n */\n event PlatformFeeAddressSet(address addr);\n\n /**\n * @dev Emitted when the accrued fees for `affiliate` are withdrawn.\n * @param affiliate The affiliate address.\n * @param accrued The amount of fees withdrawn.\n */\n event AffiliateFeesWithdrawn(address indexed affiliate, uint256 accrued);\n\n /**\n * @dev Emitted when the accrued fees for the golden egg of `edition` are withdrawn.\n * @param edition The edition address.\n * @param receipient The receipient.\n * @param accrued The amount of fees withdrawn.\n */\n event GoldenEggFeesWithdrawn(address indexed edition, address indexed receipient, uint128 accrued);\n\n /**\n * @dev Emitted when the accrued fees for the platform are withdrawn.\n * @param accrued The amount of fees withdrawn.\n */\n event PlatformFeesWithdrawn(uint128 accrued);\n\n // =============================================================\n // ERRORS\n // =============================================================\n\n /**\n * @dev The Ether value paid is below the value required.\n * @param paid The amount sent to the contract.\n * @param required The amount required.\n */\n error Underpaid(uint256 paid, uint256 required);\n\n /**\n * @dev The Ether value paid out is below the value required.\n * @param payout The amount to pau out..\n * @param required The amount required.\n */\n error InsufficientPayout(uint256 payout, uint256 required);\n\n /**\n * @dev There is not enough tokens in the Sound Automated Market for selling back.\n * @param available The number of tokens in the Sound Automated Market.\n * @param required The amount of tokens required.\n */\n error InsufficientSupply(uint256 available, uint256 required);\n\n /**\n * @dev Cannot perform the operation after the mint has already concluded.\n */\n error MintHasConcluded();\n\n /**\n * @dev The inflection price cannot be zero.\n */\n error InflectionPriceIsZero();\n\n /**\n * @dev The inflection point cannot be zero.\n */\n error InflectionPointIsZero();\n\n /**\n * @dev The BPS for the fee cannot exceed the `MAX_PLATFORM_FEE_BPS`.\n */\n error InvalidPlatformFeeBPS();\n\n /**\n * @dev The BPS for the fee cannot exceed the `MAX_ARTIST_FEE_BPS`.\n */\n error InvalidArtistFeeBPS();\n\n /**\n * @dev The BPS for the fee cannot exceed the `MAX_AFFILAITE_FEE_BPS`.\n */\n error InvalidAffiliateFeeBPS();\n\n /**\n * @dev The BPS for the fee cannot exceed the `MAX_GOLDEN_EGG_FEE_BPS`.\n */\n error InvalidGoldenEggFeeBPS();\n\n /**\n * @dev Cannot buy.\n */\n error BuyIsFrozen();\n\n /**\n * @dev The platform fee address cannot be zero.\n */\n error PlatformFeeAddressIsZero();\n\n /**\n * @dev There already is a Sound Automated Market for `edition`.\n */\n error SAMAlreadyExists();\n\n /**\n * @dev There is no Sound Automated Market for `edition`.\n */\n error SAMDoesNotExist();\n\n // =============================================================\n // PUBLIC / EXTERNAL WRITE FUNCTIONS\n // =============================================================\n\n /**\n * @dev Creates a Sound Automated Market on `edition`.\n * @param edition The edition address.\n * @param basePrice The price added to the bonding curve price.\n * @param inflectionPrice The sigmoid inflection price of the bonding curve.\n * @param inflectionPoint The sigmoid inflection point of the bonding curve.\n * @param artistFeeBPS The fee BPS (basis points) to pay the artist.\n * @param goldenEggFeeBPS The fee BPS (basis points) to pay the golden egg holder.\n * @param affiliateFeeBPS The fee BPS (basis points) to pay affiliates.\n */\n function create(\n address edition,\n uint96 basePrice,\n uint96 inflectionPrice,\n uint32 inflectionPoint,\n uint16 artistFeeBPS,\n uint16 goldenEggFeeBPS,\n uint16 affiliateFeeBPS\n ) external;\n\n /**\n * @dev Mints (buys) tokens for a given edition.\n * @param edition The edition address.\n * @param to The address to mint to.\n * @param quantity Token quantity to mint in song `edition`.\n * @param affiliate The affiliate address.\n * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any.\n */\n function buy(\n address edition,\n address to,\n uint32 quantity,\n address affiliate,\n bytes32[] calldata affiliateProof\n ) external payable;\n\n /**\n * @dev Burns (sell) tokens for a given edition.\n * @param edition The edition address.\n * @param tokenIds The token IDs to burn.\n * @param minimumPayout The minimum payout for the transaction to succeed.\n * @param payoutTo The address to send the payout to.\n */\n function sell(\n address edition,\n uint256[] calldata tokenIds,\n uint256 minimumPayout,\n address payoutTo\n ) external;\n\n /**\n * @dev Sets the base price for `edition`.\n * This will be added to the bonding curve price.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param basePrice The price added to the bonding curve price.\n */\n function setBasePrice(address edition, uint96 basePrice) external;\n\n /**\n * @dev Sets the bonding curve inflection price for `edition`.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param inflectionPrice The sigmoid inflection price of the bonding curve.\n */\n function setInflectionPrice(address edition, uint96 inflectionPrice) external;\n\n /**\n * @dev Sets the bonding curve inflection point for `edition`.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param inflectionPoint The sigmoid inflection point of the bonding curve.\n */\n function setInflectionPoint(address edition, uint32 inflectionPoint) external;\n\n /**\n * @dev Sets the artist fee for `edition`.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param bps The artist fee in basis points.\n */\n function setArtistFee(address edition, uint16 bps) external;\n\n /**\n * @dev Sets the affiliate fee for `edition`.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param bps The affiliate fee in basis points.\n */\n function setAffiliateFee(address edition, uint16 bps) external;\n\n /**\n * @dev Sets the affiliate Merkle root for (`edition`, `mintId`).\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param root The affiliate Merkle root, if any.\n */\n function setAffiliateMerkleRoot(address edition, bytes32 root) external;\n\n /**\n * @dev Sets the golden egg fee for `edition`.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n * @param bps The golden egg fee in basis points.\n */\n function setGoldenEggFee(address edition, uint16 bps) external;\n\n /**\n * @dev Closes the buy for the `edition` permanently.\n *\n * Calling conditions:\n * - The caller must be the edition's owner or admin.\n *\n * @param edition The edition address.\n */\n function freezeBuy(address edition) external;\n\n /**\n * @dev Withdraws all the accrued fees for `affiliate`.\n * @param affiliate The affiliate address.\n */\n function withdrawForAffiliate(address affiliate) external;\n\n /**\n * @dev Withdraws all the accrued fees for the platform.\n */\n function withdrawForPlatform() external;\n\n /**\n * @dev Withdraws all the accrued fees for the golden egg.\n * @param edition The edition address.\n */\n function withdrawForGoldenEgg(address edition) external;\n\n /**\n * @dev Sets the platform fee bps.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract.\n *\n * @param bps The platform fee in basis points.\n */\n function setPlatformFee(uint16 bps) external;\n\n /**\n * @dev Sets the platform fee address.\n *\n * Calling conditions:\n * - The caller must be the owner of the contract.\n *\n * @param addr The platform fee address.\n */\n function setPlatformFeeAddress(address addr) external;\n\n // =============================================================\n // PUBLIC / EXTERNAL VIEW FUNCTIONS\n // =============================================================\n\n /**\n * @dev This is the denominator, in basis points (BPS), for any of the fees.\n * @return The constant value.\n */\n function BPS_DENOMINATOR() external pure returns (uint16);\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the platform fees.\n * @return The constant value.\n */\n function MAX_PLATFORM_FEE_BPS() external pure returns (uint16);\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the artist fees.\n * @return The constant value.\n */\n function MAX_ARTIST_FEE_BPS() external pure returns (uint16);\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the affiliate fees.\n * @return The constant value.\n */\n function MAX_AFFILIATE_FEE_BPS() external pure returns (uint16);\n\n /**\n * @dev The maximum basis points (BPS) limit allowed for the golden egg fees.\n * @return The constant value.\n */\n function MAX_GOLDEN_EGG_FEE_BPS() external pure returns (uint16);\n\n /**\n * @dev Returns the platform fee basis points.\n * @return The configured value.\n */\n function platformFeeBPS() external returns (uint16);\n\n /**\n * @dev Returns the platform fee address.\n * @return The configured value.\n */\n function platformFeeAddress() external returns (address);\n\n /**\n * @dev Returns the information for the Sound Automated Market for `edition`.\n * @param edition The edition address.\n * @return The latest value.\n */\n function samInfo(address edition) external view returns (SAMInfo memory);\n\n /**\n * @dev Returns the total value under the bonding curve for `quantity`, from `fromSupply`.\n * @param edition The edition address.\n * @param fromSupply The starting number of tokens in the bonding curve.\n * @param quantity The number of tokens.\n * @return The computed value.\n */\n function totalValue(\n address edition,\n uint32 fromSupply,\n uint32 quantity\n ) external view returns (uint256);\n\n /**\n * @dev Returns the total amount of ETH required to buy from\n * `supply + supplyForwardOffset` to `supply + supplyForwardOffset + quantity`.\n * @param edition The edition address.\n * @param supplyForwardOffset The offset added to the current supply.\n * @param quantity The number of tokens.\n * @return The computed value.\n */\n function totalBuyPrice(\n address edition,\n uint32 supplyForwardOffset,\n uint32 quantity\n ) external view returns (uint256);\n\n /**\n * @dev Returns the total amount of ETH required to sell from\n * `supply - supplyBackwardOffset` to `supply - supplyBackwardOffset - quantity`.\n * @param edition The edition address.\n * @param supplyBackwardOffset The offset added to the current supply.\n * @param quantity The number of tokens.\n * @return The computed value.\n */\n function totalSellPrice(\n address edition,\n uint32 supplyBackwardOffset,\n uint32 quantity\n ) external view returns (uint256);\n\n /**\n * @dev The total fees accrued for the golden egg on `edition`.\n * @param edition The edition address.\n * @return The latest value.\n */\n function goldenEggFeesAccrued(address edition) external view returns (uint128);\n\n /**\n * @dev The receipient of the golden egg fees on `edition`.\n * If there is no golden egg winner, the `receipient` will be the `edition`.\n * @param edition The edition address.\n * @return receipient The latest value.\n */\n function goldenEggFeeRecipient(address edition) external view returns (address receipient);\n\n /**\n * @dev The total fees accrued for `affiliate`.\n * @param affiliate The affiliate's address.\n * @return The latest value.\n */\n function affiliateFeesAccrued(address affiliate) external view returns (uint128);\n\n /**\n * @dev The total fees accrued for the platform.\n * @return The latest value.\n */\n function platformFeesAccrued() external view returns (uint128);\n\n /**\n * @dev Whether `affiliate` is affiliated for `edition`.\n * @param edition The edition's address.\n * @param affiliate The affiliate's address.\n * @param affiliateProof The Merkle proof needed for verifying the affiliate, if any.\n * @return The computed value.\n */\n function isAffiliatedWithProof(\n address edition,\n address affiliate,\n bytes32[] calldata affiliateProof\n ) external view returns (bool);\n\n /**\n * @dev Whether `affiliate` is affiliated for `edition`.\n * @param edition The edition's address.\n * @param affiliate The affiliate's address.\n * @return The computed value.\n */\n function isAffiliated(address edition, address affiliate) external view returns (bool);\n\n /**\n * @dev Returns the affiliate Merkle root.\n * @param edition The edition's address.\n * @return The latest value.\n */\n function affiliateMerkleRoot(address edition) external view returns (bytes32);\n\n /**\n * @dev Returns the module's interface ID.\n * @return The constant value.\n */\n function moduleInterfaceId() external pure returns (bytes4);\n}\n"},"contracts/modules/utils/BondingCurveLib.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport \"solady/utils/FixedPointMathLib.sol\";\n\nlibrary BondingCurveLib {\n function sigmoid2(\n uint32 inflectionPoint,\n uint96 inflectionPrice,\n uint32 supply\n ) internal pure returns (uint256) {\n return sigmoid2Sum(inflectionPoint, inflectionPrice, supply, 1);\n }\n\n function sigmoid2Sum(\n uint32 inflectionPoint,\n uint96 inflectionPrice,\n uint32 fromSupply,\n uint32 quantity\n ) internal pure returns (uint256 sum) {\n // We don't need checked arithmetic for the sum because the max possible value\n // is `10384593717069655257060992658309120`,\n // with `inflectionPoint = 1` and `inflectionPrice = 2 ** 96 - 1`.\n // As `10384593717069655257060992658309120 * (2 ** 32 - 1) < 2 ** 256 - 1`,\n // overflow is not possible.\n unchecked {\n uint256 end = uint256(fromSupply) + 1 + uint256(quantity);\n uint256 g = inflectionPoint;\n uint256 h = inflectionPrice;\n for (uint256 s = uint256(fromSupply) + 1; s != end; ++s) {\n if (s >= g) {\n uint256 r = FixedPointMathLib.sqrt((s - ((3 * g) >> 2)) * g);\n sum += FixedPointMathLib.rawDiv((h * r) << 1, g);\n continue;\n }\n sum += FixedPointMathLib.rawDiv(s * s * h, g * g);\n }\n }\n }\n\n function totalBuyPriceAndFeePerBPS(\n uint256 totalSellPrice,\n uint256 totalFeeBPS,\n uint256 maxBPS\n ) internal pure returns (uint256 totalBuyPrice, uint256 feePerBPS) {\n uint256 numer = totalSellPrice * maxBPS;\n uint256 denom = maxBPS - totalFeeBPS;\n unchecked {\n // - Will revert if `denom` is zero.\n // - Won't underflow, as `numer / denom` won't be less than `totalSellPrice`.\n // - Will be zero if `totalFeeBPS` is zero.\n feePerBPS = FixedPointMathLib.rawDiv(numer / denom - totalSellPrice, totalFeeBPS);\n // - Won't overflow.\n totalBuyPrice = feePerBPS * totalFeeBPS + totalSellPrice;\n }\n }\n}\n"},"lib/ERC721A-Upgradeable/contracts/IERC721AUpgradeable.sol":{"content":"// SPDX-License-Identifier: MIT\n// ERC721A Contracts v4.2.3\n// Creator: Chiru Labs\n\npragma solidity ^0.8.4;\n\n/**\n * @dev Interface of ERC721A.\n */\ninterface IERC721AUpgradeable {\n /**\n * The caller must own the token or be an approved operator.\n */\n error ApprovalCallerNotOwnerNorApproved();\n\n /**\n * The token does not exist.\n */\n error ApprovalQueryForNonexistentToken();\n\n /**\n * Cannot query the balance for the zero address.\n */\n error BalanceQueryForZeroAddress();\n\n /**\n * Cannot mint to the zero address.\n */\n error MintToZeroAddress();\n\n /**\n * The quantity of tokens minted must be more than zero.\n */\n error MintZeroQuantity();\n\n /**\n * The token does not exist.\n */\n error OwnerQueryForNonexistentToken();\n\n /**\n * The caller must own the token or be an approved operator.\n */\n error TransferCallerNotOwnerNorApproved();\n\n /**\n * The token must be owned by `from`.\n */\n error TransferFromIncorrectOwner();\n\n /**\n * Cannot safely transfer to a contract that does not implement the\n * ERC721Receiver interface.\n */\n error TransferToNonERC721ReceiverImplementer();\n\n /**\n * Cannot transfer to the zero address.\n */\n error TransferToZeroAddress();\n\n /**\n * The token does not exist.\n */\n error URIQueryForNonexistentToken();\n\n /**\n * The `quantity` minted with ERC2309 exceeds the safety limit.\n */\n error MintERC2309QuantityExceedsLimit();\n\n /**\n * The `extraData` cannot be set on an unintialized ownership slot.\n */\n error OwnershipNotInitializedForExtraData();\n\n // =============================================================\n // STRUCTS\n // =============================================================\n\n struct TokenOwnership {\n // The address of the owner.\n address addr;\n // Stores the start time of ownership with minimal overhead for tokenomics.\n uint64 startTimestamp;\n // Whether the token has been burned.\n bool burned;\n // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}.\n uint24 extraData;\n }\n\n // =============================================================\n // TOKEN COUNTERS\n // =============================================================\n\n /**\n * @dev Returns the total number of tokens in existence.\n * Burned tokens will reduce the count.\n * To get the total number of tokens minted, please see {_totalMinted}.\n */\n function totalSupply() external view returns (uint256);\n\n // =============================================================\n // IERC165\n // =============================================================\n\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n\n // =============================================================\n // IERC721\n // =============================================================\n\n /**\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\n */\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n\n /**\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\n */\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\n\n /**\n * @dev Emitted when `owner` enables or disables\n * (`approved`) `operator` to manage all of its assets.\n */\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\n\n /**\n * @dev Returns the number of tokens in `owner`'s account.\n */\n function balanceOf(address owner) external view returns (uint256 balance);\n\n /**\n * @dev Returns the owner of the `tokenId` token.\n *\n * Requirements:\n *\n * - `tokenId` must exist.\n */\n function ownerOf(uint256 tokenId) external view returns (address owner);\n\n /**\n * @dev Safely transfers `tokenId` token from `from` to `to`,\n * checking first that contract recipients are aware of the ERC721 protocol\n * to prevent tokens from being forever locked.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must exist and be owned by `from`.\n * - If the caller is not `from`, it must be have been allowed to move\n * this token by either {approve} or {setApprovalForAll}.\n * - If `to` refers to a smart contract, it must implement\n * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\n *\n * Emits a {Transfer} event.\n */\n function safeTransferFrom(\n address from,\n address to,\n uint256 tokenId,\n bytes calldata data\n ) external payable;\n\n /**\n * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`.\n */\n function safeTransferFrom(\n address from,\n address to,\n uint256 tokenId\n ) external payable;\n\n /**\n * @dev Transfers `tokenId` from `from` to `to`.\n *\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom}\n * whenever possible.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must be owned by `from`.\n * - If the caller is not `from`, it must be approved to move this token\n * by either {approve} or {setApprovalForAll}.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(\n address from,\n address to,\n uint256 tokenId\n ) external payable;\n\n /**\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\n * The approval is cleared when the token is transferred.\n *\n * Only a single account can be approved at a time, so approving the\n * zero address clears previous approvals.\n *\n * Requirements:\n *\n * - The caller must own the token or be an approved operator.\n * - `tokenId` must exist.\n *\n * Emits an {Approval} event.\n */\n function approve(address to, uint256 tokenId) external payable;\n\n /**\n * @dev Approve or remove `operator` as an operator for the caller.\n * Operators can call {transferFrom} or {safeTransferFrom}\n * for any token owned by the caller.\n *\n * Requirements:\n *\n * - The `operator` cannot be the caller.\n *\n * Emits an {ApprovalForAll} event.\n */\n function setApprovalForAll(address operator, bool _approved) external;\n\n /**\n * @dev Returns the account approved for `tokenId` token.\n *\n * Requirements:\n *\n * - `tokenId` must exist.\n */\n function getApproved(uint256 tokenId) external view returns (address operator);\n\n /**\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\n *\n * See {setApprovalForAll}.\n */\n function isApprovedForAll(address owner, address operator) external view returns (bool);\n\n // =============================================================\n // IERC721Metadata\n // =============================================================\n\n /**\n * @dev Returns the token collection name.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the token collection symbol.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.\n */\n function tokenURI(uint256 tokenId) external view returns (string memory);\n\n // =============================================================\n // IERC2309\n // =============================================================\n\n /**\n * @dev Emitted when tokens in `fromTokenId` to `toTokenId`\n * (inclusive) is transferred from `from` to `to`, as defined in the\n * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard.\n *\n * See {_mintERC2309} for more details.\n */\n event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);\n}\n"},"lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Interface of the ERC165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n"},"lib/openzeppelin-contracts-upgradeable/contracts/interfaces/IERC2981Upgradeable.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.6.0) (interfaces/IERC2981.sol)\n\npragma solidity ^0.8.0;\n\nimport \"../utils/introspection/IERC165Upgradeable.sol\";\n\n/**\n * @dev Interface for the NFT Royalty Standard.\n *\n * A standardized way to retrieve royalty payment information for non-fungible tokens (NFTs) to enable universal\n * support for royalty payments across all NFT marketplaces and ecosystem participants.\n *\n * _Available since v4.5._\n */\ninterface IERC2981Upgradeable is IERC165Upgradeable {\n /**\n * @dev Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of\n * exchange. The royalty amount is denominated and should be paid in that same unit of exchange.\n */\n function royaltyInfo(uint256 tokenId, uint256 salePrice)\n external\n view\n returns (address receiver, uint256 royaltyAmount);\n}\n"},"lib/openzeppelin-contracts-upgradeable/contracts/utils/introspection/IERC165Upgradeable.sol":{"content":"// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Interface of the ERC165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165Upgradeable {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n"},"lib/solady/src/auth/Ownable.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice Simple single owner authorization mixin.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)\n/// @dev While the ownable portion follows [EIP-173](https://eips.ethereum.org/EIPS/eip-173)\n/// for compatibility, the nomenclature for the 2-step ownership handover\n/// may be unique to this codebase.\nabstract contract Ownable {\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CUSTOM ERRORS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The caller is not authorized to call the function.\n error Unauthorized();\n\n /// @dev The `newOwner` cannot be the zero address.\n error NewOwnerIsZeroAddress();\n\n /// @dev The `pendingOwner` does not have a valid handover request.\n error NoHandoverRequest();\n\n /// @dev `bytes4(keccak256(bytes(\"Unauthorized()\")))`.\n uint256 private constant _UNAUTHORIZED_ERROR_SELECTOR = 0x82b42900;\n\n /// @dev `bytes4(keccak256(bytes(\"NewOwnerIsZeroAddress()\")))`.\n uint256 private constant _NEW_OWNER_IS_ZERO_ADDRESS_ERROR_SELECTOR = 0x7448fbae;\n\n /// @dev `bytes4(keccak256(bytes(\"NoHandoverRequest()\")))`.\n uint256 private constant _NO_HANDOVER_REQUEST_ERROR_SELECTOR = 0x6f5e8818;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* EVENTS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The ownership is transferred from `oldOwner` to `newOwner`.\n /// This event is intentionally kept the same as OpenZeppelin's Ownable to be\n /// compatible with indexers and [EIP-173](https://eips.ethereum.org/EIPS/eip-173),\n /// despite it not being as lightweight as a single argument event.\n event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);\n\n /// @dev An ownership handover to `pendingOwner` has been requested.\n event OwnershipHandoverRequested(address indexed pendingOwner);\n\n /// @dev The ownership handover to `pendingOwner` has been canceled.\n event OwnershipHandoverCanceled(address indexed pendingOwner);\n\n /// @dev `keccak256(bytes(\"OwnershipTransferred(address,address)\"))`.\n uint256 private constant _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE =\n 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0;\n\n /// @dev `keccak256(bytes(\"OwnershipHandoverRequested(address)\"))`.\n uint256 private constant _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE =\n 0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d;\n\n /// @dev `keccak256(bytes(\"OwnershipHandoverCanceled(address)\"))`.\n uint256 private constant _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE =\n 0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* STORAGE */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The owner slot is given by: `not(_OWNER_SLOT_NOT)`.\n /// It is intentionally choosen to be a high value\n /// to avoid collision with lower slots.\n /// The choice of manual storage layout is to enable compatibility\n /// with both regular and upgradeable contracts.\n uint256 private constant _OWNER_SLOT_NOT = 0x8b78c6d8;\n\n /// The ownership handover slot of `newOwner` is given by:\n /// ```\n /// mstore(0x00, or(shl(96, user), _HANDOVER_SLOT_SEED))\n /// let handoverSlot := keccak256(0x00, 0x20)\n /// ```\n /// It stores the expiry timestamp of the two-step ownership handover.\n uint256 private constant _HANDOVER_SLOT_SEED = 0x389a75e1;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* INTERNAL FUNCTIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Initializes the owner directly without authorization guard.\n /// This function must be called upon initialization,\n /// regardless of whether the contract is upgradeable or not.\n /// This is to enable generalization to both regular and upgradeable contracts,\n /// and to save gas in case the initial owner is not the caller.\n /// For performance reasons, this function will not check if there\n /// is an existing owner.\n function _initializeOwner(address newOwner) internal virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // Clean the upper 96 bits.\n newOwner := shr(96, shl(96, newOwner))\n // Store the new value.\n sstore(not(_OWNER_SLOT_NOT), newOwner)\n // Emit the {OwnershipTransferred} event.\n log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)\n }\n }\n\n /// @dev Sets the owner directly without authorization guard.\n function _setOwner(address newOwner) internal virtual {\n /// @solidity memory-safe-assembly\n assembly {\n let ownerSlot := not(_OWNER_SLOT_NOT)\n // Clean the upper 96 bits.\n newOwner := shr(96, shl(96, newOwner))\n // Emit the {OwnershipTransferred} event.\n log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)\n // Store the new value.\n sstore(ownerSlot, newOwner)\n }\n }\n\n /// @dev Throws if the sender is not the owner.\n function _checkOwner() internal view virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // If the caller is not the stored owner, revert.\n if iszero(eq(caller(), sload(not(_OWNER_SLOT_NOT)))) {\n mstore(0x00, _UNAUTHORIZED_ERROR_SELECTOR)\n revert(0x1c, 0x04)\n }\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* PUBLIC UPDATE FUNCTIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Allows the owner to transfer the ownership to `newOwner`.\n function transferOwnership(address newOwner) public payable virtual onlyOwner {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(shl(96, newOwner)) {\n mstore(0x00, _NEW_OWNER_IS_ZERO_ADDRESS_ERROR_SELECTOR)\n revert(0x1c, 0x04)\n }\n }\n _setOwner(newOwner);\n }\n\n /// @dev Allows the owner to renounce their ownership.\n function renounceOwnership() public payable virtual onlyOwner {\n _setOwner(address(0));\n }\n\n /// @dev Request a two-step ownership handover to the caller.\n /// The request will be automatically expire in 48 hours (172800 seconds) by default.\n function requestOwnershipHandover() public payable virtual {\n unchecked {\n uint256 expires = block.timestamp + ownershipHandoverValidFor();\n /// @solidity memory-safe-assembly\n assembly {\n // Compute and set the handover slot to `expires`.\n mstore(0x0c, _HANDOVER_SLOT_SEED)\n mstore(0x00, caller())\n sstore(keccak256(0x0c, 0x20), expires)\n // Emit the {OwnershipHandoverRequested} event.\n log2(0, 0, _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE, caller())\n }\n }\n }\n\n /// @dev Cancels the two-step ownership handover to the caller, if any.\n function cancelOwnershipHandover() public payable virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute and set the handover slot to 0.\n mstore(0x0c, _HANDOVER_SLOT_SEED)\n mstore(0x00, caller())\n sstore(keccak256(0x0c, 0x20), 0)\n // Emit the {OwnershipHandoverCanceled} event.\n log2(0, 0, _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE, caller())\n }\n }\n\n /// @dev Allows the owner to complete the two-step ownership handover to `pendingOwner`.\n /// Reverts if there is no existing ownership handover requested by `pendingOwner`.\n function completeOwnershipHandover(address pendingOwner) public payable virtual onlyOwner {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute and set the handover slot to 0.\n mstore(0x0c, _HANDOVER_SLOT_SEED)\n mstore(0x00, pendingOwner)\n let handoverSlot := keccak256(0x0c, 0x20)\n // If the handover does not exist, or has expired.\n if gt(timestamp(), sload(handoverSlot)) {\n mstore(0x00, _NO_HANDOVER_REQUEST_ERROR_SELECTOR)\n revert(0x1c, 0x04)\n }\n // Set the handover slot to 0.\n sstore(handoverSlot, 0)\n }\n _setOwner(pendingOwner);\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* PUBLIC READ FUNCTIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Returns the owner of the contract.\n function owner() public view virtual returns (address result) {\n /// @solidity memory-safe-assembly\n assembly {\n result := sload(not(_OWNER_SLOT_NOT))\n }\n }\n\n /// @dev Returns the expiry timestamp for the two-step ownership handover to `pendingOwner`.\n function ownershipHandoverExpiresAt(address pendingOwner)\n public\n view\n virtual\n returns (uint256 result)\n {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the handover slot.\n mstore(0x0c, _HANDOVER_SLOT_SEED)\n mstore(0x00, pendingOwner)\n // Load the handover slot.\n result := sload(keccak256(0x0c, 0x20))\n }\n }\n\n /// @dev Returns how long a two-step ownership handover is valid for in seconds.\n function ownershipHandoverValidFor() public view virtual returns (uint64) {\n return 48 * 3600;\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* MODIFIERS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Marks a function as only callable by the owner.\n modifier onlyOwner() virtual {\n _checkOwner();\n _;\n }\n}\n"},"lib/solady/src/auth/OwnableRoles.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\nimport \"./Ownable.sol\";\n\n/// @notice Simple single owner and multiroles authorization mixin.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)\n/// @dev While the ownable portion follows [EIP-173](https://eips.ethereum.org/EIPS/eip-173)\n/// for compatibility, the nomenclature for the 2-step ownership handover and roles\n/// may be unique to this codebase.\nabstract contract OwnableRoles is Ownable {\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CUSTOM ERRORS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev `bytes4(keccak256(bytes(\"Unauthorized()\")))`.\n uint256 private constant _UNAUTHORIZED_ERROR_SELECTOR = 0x82b42900;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* EVENTS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The `user`'s roles is updated to `roles`.\n /// Each bit of `roles` represents whether the role is set.\n event RolesUpdated(address indexed user, uint256 indexed roles);\n\n /// @dev `keccak256(bytes(\"RolesUpdated(address,uint256)\"))`.\n uint256 private constant _ROLES_UPDATED_EVENT_SIGNATURE =\n 0x715ad5ce61fc9595c7b415289d59cf203f23a94fa06f04af7e489a0a76e1fe26;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* STORAGE */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The role slot of `user` is given by:\n /// ```\n /// mstore(0x00, or(shl(96, user), _ROLE_SLOT_SEED))\n /// let roleSlot := keccak256(0x00, 0x20)\n /// ```\n /// This automatically ignores the upper bits of the `user` in case\n /// they are not clean, as well as keep the `keccak256` under 32-bytes.\n ///\n /// Note: This is equal to `_OWNER_SLOT_NOT` in for gas efficiency.\n uint256 private constant _ROLE_SLOT_SEED = 0x8b78c6d8;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* INTERNAL FUNCTIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Grants the roles directly without authorization guard.\n /// Each bit of `roles` represents the role to turn on.\n function _grantRoles(address user, uint256 roles) internal virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, user)\n let roleSlot := keccak256(0x0c, 0x20)\n // Load the current value and `or` it with `roles`.\n roles := or(sload(roleSlot), roles)\n // Store the new value.\n sstore(roleSlot, roles)\n // Emit the {RolesUpdated} event.\n log3(0, 0, _ROLES_UPDATED_EVENT_SIGNATURE, shr(96, mload(0x0c)), roles)\n }\n }\n\n /// @dev Removes the roles directly without authorization guard.\n /// Each bit of `roles` represents the role to turn off.\n function _removeRoles(address user, uint256 roles) internal virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, user)\n let roleSlot := keccak256(0x0c, 0x20)\n // Load the current value.\n let currentRoles := sload(roleSlot)\n // Use `and` to compute the intersection of `currentRoles` and `roles`,\n // `xor` it with `currentRoles` to flip the bits in the intersection.\n roles := xor(currentRoles, and(currentRoles, roles))\n // Then, store the new value.\n sstore(roleSlot, roles)\n // Emit the {RolesUpdated} event.\n log3(0, 0, _ROLES_UPDATED_EVENT_SIGNATURE, shr(96, mload(0x0c)), roles)\n }\n }\n\n /// @dev Throws if the sender does not have any of the `roles`.\n function _checkRoles(uint256 roles) internal view virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, caller())\n // Load the stored value, and if the `and` intersection\n // of the value and `roles` is zero, revert.\n if iszero(and(sload(keccak256(0x0c, 0x20)), roles)) {\n mstore(0x00, _UNAUTHORIZED_ERROR_SELECTOR)\n revert(0x1c, 0x04)\n }\n }\n }\n\n /// @dev Throws if the sender is not the owner,\n /// and does not have any of the `roles`.\n /// Checks for ownership first, then lazily checks for roles.\n function _checkOwnerOrRoles(uint256 roles) internal view virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // If the caller is not the stored owner.\n // Note: `_ROLE_SLOT_SEED` is equal to `_OWNER_SLOT_NOT`.\n if iszero(eq(caller(), sload(not(_ROLE_SLOT_SEED)))) {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, caller())\n // Load the stored value, and if the `and` intersection\n // of the value and `roles` is zero, revert.\n if iszero(and(sload(keccak256(0x0c, 0x20)), roles)) {\n mstore(0x00, _UNAUTHORIZED_ERROR_SELECTOR)\n revert(0x1c, 0x04)\n }\n }\n }\n }\n\n /// @dev Throws if the sender does not have any of the `roles`,\n /// and is not the owner.\n /// Checks for roles first, then lazily checks for ownership.\n function _checkRolesOrOwner(uint256 roles) internal view virtual {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, caller())\n // Load the stored value, and if the `and` intersection\n // of the value and `roles` is zero, revert.\n if iszero(and(sload(keccak256(0x0c, 0x20)), roles)) {\n // If the caller is not the stored owner.\n // Note: `_ROLE_SLOT_SEED` is equal to `_OWNER_SLOT_NOT`.\n if iszero(eq(caller(), sload(not(_ROLE_SLOT_SEED)))) {\n mstore(0x00, _UNAUTHORIZED_ERROR_SELECTOR)\n revert(0x1c, 0x04)\n }\n }\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* PUBLIC UPDATE FUNCTIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Allows the owner to grant `user` `roles`.\n /// If the `user` already has a role, then it will be an no-op for the role.\n function grantRoles(address user, uint256 roles) public payable virtual onlyOwner {\n _grantRoles(user, roles);\n }\n\n /// @dev Allows the owner to remove `user` `roles`.\n /// If the `user` does not have a role, then it will be an no-op for the role.\n function revokeRoles(address user, uint256 roles) public payable virtual onlyOwner {\n _removeRoles(user, roles);\n }\n\n /// @dev Allow the caller to remove their own roles.\n /// If the caller does not have a role, then it will be an no-op for the role.\n function renounceRoles(uint256 roles) public payable virtual {\n _removeRoles(msg.sender, roles);\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* PUBLIC READ FUNCTIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Returns whether `user` has any of `roles`.\n function hasAnyRole(address user, uint256 roles) public view virtual returns (bool result) {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, user)\n // Load the stored value, and set the result to whether the\n // `and` intersection of the value and `roles` is not zero.\n result := iszero(iszero(and(sload(keccak256(0x0c, 0x20)), roles)))\n }\n }\n\n /// @dev Returns whether `user` has all of `roles`.\n function hasAllRoles(address user, uint256 roles) public view virtual returns (bool result) {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, user)\n // Whether the stored value is contains all the set bits in `roles`.\n result := eq(and(sload(keccak256(0x0c, 0x20)), roles), roles)\n }\n }\n\n /// @dev Returns the roles of `user`.\n function rolesOf(address user) public view virtual returns (uint256 roles) {\n /// @solidity memory-safe-assembly\n assembly {\n // Compute the role slot.\n mstore(0x0c, _ROLE_SLOT_SEED)\n mstore(0x00, user)\n // Load the stored value.\n roles := sload(keccak256(0x0c, 0x20))\n }\n }\n\n /// @dev Convenience function to return a `roles` bitmap from an array of `ordinals`.\n /// This is meant for frontends like Etherscan, and is therefore not fully optimized.\n /// Not recommended to be called on-chain.\n function rolesFromOrdinals(uint8[] memory ordinals) public pure returns (uint256 roles) {\n /// @solidity memory-safe-assembly\n assembly {\n for { let i := shl(5, mload(ordinals)) } i { i := sub(i, 0x20) } {\n // We don't need to mask the values of `ordinals`, as Solidity\n // cleans dirty upper bits when storing variables into memory.\n roles := or(shl(mload(add(ordinals, i)), 1), roles)\n }\n }\n }\n\n /// @dev Convenience function to return an array of `ordinals` from the `roles` bitmap.\n /// This is meant for frontends like Etherscan, and is therefore not fully optimized.\n /// Not recommended to be called on-chain.\n function ordinalsFromRoles(uint256 roles) public pure returns (uint8[] memory ordinals) {\n /// @solidity memory-safe-assembly\n assembly {\n // Grab the pointer to the free memory.\n ordinals := mload(0x40)\n let ptr := add(ordinals, 0x20)\n let o := 0\n // The absence of lookup tables, De Bruijn, etc., here is intentional for\n // smaller bytecode, as this function is not meant to be called on-chain.\n for { let t := roles } 1 {} {\n mstore(ptr, o)\n // `shr` 5 is equivalent to multiplying by 0x20.\n // Push back into the ordinals array if the bit is set.\n ptr := add(ptr, shl(5, and(t, 1)))\n o := add(o, 1)\n t := shr(o, roles)\n if iszero(t) { break }\n }\n // Store the length of `ordinals`.\n mstore(ordinals, shr(5, sub(ptr, add(ordinals, 0x20))))\n // Allocate the memory.\n mstore(0x40, ptr)\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* MODIFIERS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Marks a function as only callable by an account with `roles`.\n modifier onlyRoles(uint256 roles) virtual {\n _checkRoles(roles);\n _;\n }\n\n /// @dev Marks a function as only callable by the owner or by an account\n /// with `roles`. Checks for ownership first, then lazily checks for roles.\n modifier onlyOwnerOrRoles(uint256 roles) virtual {\n _checkOwnerOrRoles(roles);\n _;\n }\n\n /// @dev Marks a function as only callable by an account with `roles`\n /// or the owner. Checks for roles first, then lazily checks for ownership.\n modifier onlyRolesOrOwner(uint256 roles) virtual {\n _checkRolesOrOwner(roles);\n _;\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* ROLE CONSTANTS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n // IYKYK\n\n uint256 internal constant _ROLE_0 = 1 << 0;\n uint256 internal constant _ROLE_1 = 1 << 1;\n uint256 internal constant _ROLE_2 = 1 << 2;\n uint256 internal constant _ROLE_3 = 1 << 3;\n uint256 internal constant _ROLE_4 = 1 << 4;\n uint256 internal constant _ROLE_5 = 1 << 5;\n uint256 internal constant _ROLE_6 = 1 << 6;\n uint256 internal constant _ROLE_7 = 1 << 7;\n uint256 internal constant _ROLE_8 = 1 << 8;\n uint256 internal constant _ROLE_9 = 1 << 9;\n uint256 internal constant _ROLE_10 = 1 << 10;\n uint256 internal constant _ROLE_11 = 1 << 11;\n uint256 internal constant _ROLE_12 = 1 << 12;\n uint256 internal constant _ROLE_13 = 1 << 13;\n uint256 internal constant _ROLE_14 = 1 << 14;\n uint256 internal constant _ROLE_15 = 1 << 15;\n uint256 internal constant _ROLE_16 = 1 << 16;\n uint256 internal constant _ROLE_17 = 1 << 17;\n uint256 internal constant _ROLE_18 = 1 << 18;\n uint256 internal constant _ROLE_19 = 1 << 19;\n uint256 internal constant _ROLE_20 = 1 << 20;\n uint256 internal constant _ROLE_21 = 1 << 21;\n uint256 internal constant _ROLE_22 = 1 << 22;\n uint256 internal constant _ROLE_23 = 1 << 23;\n uint256 internal constant _ROLE_24 = 1 << 24;\n uint256 internal constant _ROLE_25 = 1 << 25;\n uint256 internal constant _ROLE_26 = 1 << 26;\n uint256 internal constant _ROLE_27 = 1 << 27;\n uint256 internal constant _ROLE_28 = 1 << 28;\n uint256 internal constant _ROLE_29 = 1 << 29;\n uint256 internal constant _ROLE_30 = 1 << 30;\n uint256 internal constant _ROLE_31 = 1 << 31;\n uint256 internal constant _ROLE_32 = 1 << 32;\n uint256 internal constant _ROLE_33 = 1 << 33;\n uint256 internal constant _ROLE_34 = 1 << 34;\n uint256 internal constant _ROLE_35 = 1 << 35;\n uint256 internal constant _ROLE_36 = 1 << 36;\n uint256 internal constant _ROLE_37 = 1 << 37;\n uint256 internal constant _ROLE_38 = 1 << 38;\n uint256 internal constant _ROLE_39 = 1 << 39;\n uint256 internal constant _ROLE_40 = 1 << 40;\n uint256 internal constant _ROLE_41 = 1 << 41;\n uint256 internal constant _ROLE_42 = 1 << 42;\n uint256 internal constant _ROLE_43 = 1 << 43;\n uint256 internal constant _ROLE_44 = 1 << 44;\n uint256 internal constant _ROLE_45 = 1 << 45;\n uint256 internal constant _ROLE_46 = 1 << 46;\n uint256 internal constant _ROLE_47 = 1 << 47;\n uint256 internal constant _ROLE_48 = 1 << 48;\n uint256 internal constant _ROLE_49 = 1 << 49;\n uint256 internal constant _ROLE_50 = 1 << 50;\n uint256 internal constant _ROLE_51 = 1 << 51;\n uint256 internal constant _ROLE_52 = 1 << 52;\n uint256 internal constant _ROLE_53 = 1 << 53;\n uint256 internal constant _ROLE_54 = 1 << 54;\n uint256 internal constant _ROLE_55 = 1 << 55;\n uint256 internal constant _ROLE_56 = 1 << 56;\n uint256 internal constant _ROLE_57 = 1 << 57;\n uint256 internal constant _ROLE_58 = 1 << 58;\n uint256 internal constant _ROLE_59 = 1 << 59;\n uint256 internal constant _ROLE_60 = 1 << 60;\n uint256 internal constant _ROLE_61 = 1 << 61;\n uint256 internal constant _ROLE_62 = 1 << 62;\n uint256 internal constant _ROLE_63 = 1 << 63;\n uint256 internal constant _ROLE_64 = 1 << 64;\n uint256 internal constant _ROLE_65 = 1 << 65;\n uint256 internal constant _ROLE_66 = 1 << 66;\n uint256 internal constant _ROLE_67 = 1 << 67;\n uint256 internal constant _ROLE_68 = 1 << 68;\n uint256 internal constant _ROLE_69 = 1 << 69;\n uint256 internal constant _ROLE_70 = 1 << 70;\n uint256 internal constant _ROLE_71 = 1 << 71;\n uint256 internal constant _ROLE_72 = 1 << 72;\n uint256 internal constant _ROLE_73 = 1 << 73;\n uint256 internal constant _ROLE_74 = 1 << 74;\n uint256 internal constant _ROLE_75 = 1 << 75;\n uint256 internal constant _ROLE_76 = 1 << 76;\n uint256 internal constant _ROLE_77 = 1 << 77;\n uint256 internal constant _ROLE_78 = 1 << 78;\n uint256 internal constant _ROLE_79 = 1 << 79;\n uint256 internal constant _ROLE_80 = 1 << 80;\n uint256 internal constant _ROLE_81 = 1 << 81;\n uint256 internal constant _ROLE_82 = 1 << 82;\n uint256 internal constant _ROLE_83 = 1 << 83;\n uint256 internal constant _ROLE_84 = 1 << 84;\n uint256 internal constant _ROLE_85 = 1 << 85;\n uint256 internal constant _ROLE_86 = 1 << 86;\n uint256 internal constant _ROLE_87 = 1 << 87;\n uint256 internal constant _ROLE_88 = 1 << 88;\n uint256 internal constant _ROLE_89 = 1 << 89;\n uint256 internal constant _ROLE_90 = 1 << 90;\n uint256 internal constant _ROLE_91 = 1 << 91;\n uint256 internal constant _ROLE_92 = 1 << 92;\n uint256 internal constant _ROLE_93 = 1 << 93;\n uint256 internal constant _ROLE_94 = 1 << 94;\n uint256 internal constant _ROLE_95 = 1 << 95;\n uint256 internal constant _ROLE_96 = 1 << 96;\n uint256 internal constant _ROLE_97 = 1 << 97;\n uint256 internal constant _ROLE_98 = 1 << 98;\n uint256 internal constant _ROLE_99 = 1 << 99;\n uint256 internal constant _ROLE_100 = 1 << 100;\n uint256 internal constant _ROLE_101 = 1 << 101;\n uint256 internal constant _ROLE_102 = 1 << 102;\n uint256 internal constant _ROLE_103 = 1 << 103;\n uint256 internal constant _ROLE_104 = 1 << 104;\n uint256 internal constant _ROLE_105 = 1 << 105;\n uint256 internal constant _ROLE_106 = 1 << 106;\n uint256 internal constant _ROLE_107 = 1 << 107;\n uint256 internal constant _ROLE_108 = 1 << 108;\n uint256 internal constant _ROLE_109 = 1 << 109;\n uint256 internal constant _ROLE_110 = 1 << 110;\n uint256 internal constant _ROLE_111 = 1 << 111;\n uint256 internal constant _ROLE_112 = 1 << 112;\n uint256 internal constant _ROLE_113 = 1 << 113;\n uint256 internal constant _ROLE_114 = 1 << 114;\n uint256 internal constant _ROLE_115 = 1 << 115;\n uint256 internal constant _ROLE_116 = 1 << 116;\n uint256 internal constant _ROLE_117 = 1 << 117;\n uint256 internal constant _ROLE_118 = 1 << 118;\n uint256 internal constant _ROLE_119 = 1 << 119;\n uint256 internal constant _ROLE_120 = 1 << 120;\n uint256 internal constant _ROLE_121 = 1 << 121;\n uint256 internal constant _ROLE_122 = 1 << 122;\n uint256 internal constant _ROLE_123 = 1 << 123;\n uint256 internal constant _ROLE_124 = 1 << 124;\n uint256 internal constant _ROLE_125 = 1 << 125;\n uint256 internal constant _ROLE_126 = 1 << 126;\n uint256 internal constant _ROLE_127 = 1 << 127;\n uint256 internal constant _ROLE_128 = 1 << 128;\n uint256 internal constant _ROLE_129 = 1 << 129;\n uint256 internal constant _ROLE_130 = 1 << 130;\n uint256 internal constant _ROLE_131 = 1 << 131;\n uint256 internal constant _ROLE_132 = 1 << 132;\n uint256 internal constant _ROLE_133 = 1 << 133;\n uint256 internal constant _ROLE_134 = 1 << 134;\n uint256 internal constant _ROLE_135 = 1 << 135;\n uint256 internal constant _ROLE_136 = 1 << 136;\n uint256 internal constant _ROLE_137 = 1 << 137;\n uint256 internal constant _ROLE_138 = 1 << 138;\n uint256 internal constant _ROLE_139 = 1 << 139;\n uint256 internal constant _ROLE_140 = 1 << 140;\n uint256 internal constant _ROLE_141 = 1 << 141;\n uint256 internal constant _ROLE_142 = 1 << 142;\n uint256 internal constant _ROLE_143 = 1 << 143;\n uint256 internal constant _ROLE_144 = 1 << 144;\n uint256 internal constant _ROLE_145 = 1 << 145;\n uint256 internal constant _ROLE_146 = 1 << 146;\n uint256 internal constant _ROLE_147 = 1 << 147;\n uint256 internal constant _ROLE_148 = 1 << 148;\n uint256 internal constant _ROLE_149 = 1 << 149;\n uint256 internal constant _ROLE_150 = 1 << 150;\n uint256 internal constant _ROLE_151 = 1 << 151;\n uint256 internal constant _ROLE_152 = 1 << 152;\n uint256 internal constant _ROLE_153 = 1 << 153;\n uint256 internal constant _ROLE_154 = 1 << 154;\n uint256 internal constant _ROLE_155 = 1 << 155;\n uint256 internal constant _ROLE_156 = 1 << 156;\n uint256 internal constant _ROLE_157 = 1 << 157;\n uint256 internal constant _ROLE_158 = 1 << 158;\n uint256 internal constant _ROLE_159 = 1 << 159;\n uint256 internal constant _ROLE_160 = 1 << 160;\n uint256 internal constant _ROLE_161 = 1 << 161;\n uint256 internal constant _ROLE_162 = 1 << 162;\n uint256 internal constant _ROLE_163 = 1 << 163;\n uint256 internal constant _ROLE_164 = 1 << 164;\n uint256 internal constant _ROLE_165 = 1 << 165;\n uint256 internal constant _ROLE_166 = 1 << 166;\n uint256 internal constant _ROLE_167 = 1 << 167;\n uint256 internal constant _ROLE_168 = 1 << 168;\n uint256 internal constant _ROLE_169 = 1 << 169;\n uint256 internal constant _ROLE_170 = 1 << 170;\n uint256 internal constant _ROLE_171 = 1 << 171;\n uint256 internal constant _ROLE_172 = 1 << 172;\n uint256 internal constant _ROLE_173 = 1 << 173;\n uint256 internal constant _ROLE_174 = 1 << 174;\n uint256 internal constant _ROLE_175 = 1 << 175;\n uint256 internal constant _ROLE_176 = 1 << 176;\n uint256 internal constant _ROLE_177 = 1 << 177;\n uint256 internal constant _ROLE_178 = 1 << 178;\n uint256 internal constant _ROLE_179 = 1 << 179;\n uint256 internal constant _ROLE_180 = 1 << 180;\n uint256 internal constant _ROLE_181 = 1 << 181;\n uint256 internal constant _ROLE_182 = 1 << 182;\n uint256 internal constant _ROLE_183 = 1 << 183;\n uint256 internal constant _ROLE_184 = 1 << 184;\n uint256 internal constant _ROLE_185 = 1 << 185;\n uint256 internal constant _ROLE_186 = 1 << 186;\n uint256 internal constant _ROLE_187 = 1 << 187;\n uint256 internal constant _ROLE_188 = 1 << 188;\n uint256 internal constant _ROLE_189 = 1 << 189;\n uint256 internal constant _ROLE_190 = 1 << 190;\n uint256 internal constant _ROLE_191 = 1 << 191;\n uint256 internal constant _ROLE_192 = 1 << 192;\n uint256 internal constant _ROLE_193 = 1 << 193;\n uint256 internal constant _ROLE_194 = 1 << 194;\n uint256 internal constant _ROLE_195 = 1 << 195;\n uint256 internal constant _ROLE_196 = 1 << 196;\n uint256 internal constant _ROLE_197 = 1 << 197;\n uint256 internal constant _ROLE_198 = 1 << 198;\n uint256 internal constant _ROLE_199 = 1 << 199;\n uint256 internal constant _ROLE_200 = 1 << 200;\n uint256 internal constant _ROLE_201 = 1 << 201;\n uint256 internal constant _ROLE_202 = 1 << 202;\n uint256 internal constant _ROLE_203 = 1 << 203;\n uint256 internal constant _ROLE_204 = 1 << 204;\n uint256 internal constant _ROLE_205 = 1 << 205;\n uint256 internal constant _ROLE_206 = 1 << 206;\n uint256 internal constant _ROLE_207 = 1 << 207;\n uint256 internal constant _ROLE_208 = 1 << 208;\n uint256 internal constant _ROLE_209 = 1 << 209;\n uint256 internal constant _ROLE_210 = 1 << 210;\n uint256 internal constant _ROLE_211 = 1 << 211;\n uint256 internal constant _ROLE_212 = 1 << 212;\n uint256 internal constant _ROLE_213 = 1 << 213;\n uint256 internal constant _ROLE_214 = 1 << 214;\n uint256 internal constant _ROLE_215 = 1 << 215;\n uint256 internal constant _ROLE_216 = 1 << 216;\n uint256 internal constant _ROLE_217 = 1 << 217;\n uint256 internal constant _ROLE_218 = 1 << 218;\n uint256 internal constant _ROLE_219 = 1 << 219;\n uint256 internal constant _ROLE_220 = 1 << 220;\n uint256 internal constant _ROLE_221 = 1 << 221;\n uint256 internal constant _ROLE_222 = 1 << 222;\n uint256 internal constant _ROLE_223 = 1 << 223;\n uint256 internal constant _ROLE_224 = 1 << 224;\n uint256 internal constant _ROLE_225 = 1 << 225;\n uint256 internal constant _ROLE_226 = 1 << 226;\n uint256 internal constant _ROLE_227 = 1 << 227;\n uint256 internal constant _ROLE_228 = 1 << 228;\n uint256 internal constant _ROLE_229 = 1 << 229;\n uint256 internal constant _ROLE_230 = 1 << 230;\n uint256 internal constant _ROLE_231 = 1 << 231;\n uint256 internal constant _ROLE_232 = 1 << 232;\n uint256 internal constant _ROLE_233 = 1 << 233;\n uint256 internal constant _ROLE_234 = 1 << 234;\n uint256 internal constant _ROLE_235 = 1 << 235;\n uint256 internal constant _ROLE_236 = 1 << 236;\n uint256 internal constant _ROLE_237 = 1 << 237;\n uint256 internal constant _ROLE_238 = 1 << 238;\n uint256 internal constant _ROLE_239 = 1 << 239;\n uint256 internal constant _ROLE_240 = 1 << 240;\n uint256 internal constant _ROLE_241 = 1 << 241;\n uint256 internal constant _ROLE_242 = 1 << 242;\n uint256 internal constant _ROLE_243 = 1 << 243;\n uint256 internal constant _ROLE_244 = 1 << 244;\n uint256 internal constant _ROLE_245 = 1 << 245;\n uint256 internal constant _ROLE_246 = 1 << 246;\n uint256 internal constant _ROLE_247 = 1 << 247;\n uint256 internal constant _ROLE_248 = 1 << 248;\n uint256 internal constant _ROLE_249 = 1 << 249;\n uint256 internal constant _ROLE_250 = 1 << 250;\n uint256 internal constant _ROLE_251 = 1 << 251;\n uint256 internal constant _ROLE_252 = 1 << 252;\n uint256 internal constant _ROLE_253 = 1 << 253;\n uint256 internal constant _ROLE_254 = 1 << 254;\n uint256 internal constant _ROLE_255 = 1 << 255;\n}\n"},"lib/solady/src/utils/FixedPointMathLib.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice Arithmetic library with operations for fixed-point numbers.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol)\n/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)\nlibrary FixedPointMathLib {\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CUSTOM ERRORS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The operation failed, as the output exceeds the maximum value of uint256.\n error ExpOverflow();\n\n /// @dev The operation failed, as the output exceeds the maximum value of uint256.\n error FactorialOverflow();\n\n /// @dev The operation failed, due to an multiplication overflow.\n error MulWadFailed();\n\n /// @dev The operation failed, either due to a\n /// multiplication overflow, or a division by a zero.\n error DivWadFailed();\n\n /// @dev The multiply-divide operation failed, either due to a\n /// multiplication overflow, or a division by a zero.\n error MulDivFailed();\n\n /// @dev The division failed, as the denominator is zero.\n error DivFailed();\n\n /// @dev The full precision multiply-divide operation failed, either due\n /// to the result being larger than 256 bits, or a division by a zero.\n error FullMulDivFailed();\n\n /// @dev The output is undefined, as the input is less-than-or-equal to zero.\n error LnWadUndefined();\n\n /// @dev The output is undefined, as the input is zero.\n error Log2Undefined();\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CONSTANTS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The scalar of ETH and most ERC20s.\n uint256 internal constant WAD = 1e18;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* SIMPLIFIED FIXED POINT OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Equivalent to `(x * y) / WAD` rounded down.\n function mulWad(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.\n if mul(y, gt(x, div(not(0), y))) {\n // Store the function selector of `MulWadFailed()`.\n mstore(0x00, 0xbac65e5b)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := div(mul(x, y), WAD)\n }\n }\n\n /// @dev Equivalent to `(x * y) / WAD` rounded up.\n function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.\n if mul(y, gt(x, div(not(0), y))) {\n // Store the function selector of `MulWadFailed()`.\n mstore(0x00, 0xbac65e5b)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))\n }\n }\n\n /// @dev Equivalent to `(x * WAD) / y` rounded down.\n function divWad(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.\n if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {\n // Store the function selector of `DivWadFailed()`.\n mstore(0x00, 0x7c5f487d)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := div(mul(x, WAD), y)\n }\n }\n\n /// @dev Equivalent to `(x * WAD) / y` rounded up.\n function divWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // Equivalent to `require(y != 0 && (WAD == 0 || x <= type(uint256).max / WAD))`.\n if iszero(mul(y, iszero(mul(WAD, gt(x, div(not(0), WAD)))))) {\n // Store the function selector of `DivWadFailed()`.\n mstore(0x00, 0x7c5f487d)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := add(iszero(iszero(mod(mul(x, WAD), y))), div(mul(x, WAD), y))\n }\n }\n\n /// @dev Equivalent to `x` to the power of `y`.\n /// because `x ** y = (e ** ln(x)) ** y = e ** (ln(x) * y)`.\n function powWad(int256 x, int256 y) internal pure returns (int256) {\n // Using `ln(x)` means `x` must be greater than 0.\n return expWad((lnWad(x) * y) / int256(WAD));\n }\n\n /// @dev Returns `exp(x)`, denominated in `WAD`.\n function expWad(int256 x) internal pure returns (int256 r) {\n unchecked {\n // When the result is < 0.5 we return zero. This happens when\n // x <= floor(log(0.5e18) * 1e18) ~ -42e18\n if (x <= -42139678854452767551) return r;\n\n /// @solidity memory-safe-assembly\n assembly {\n // When the result is > (2**255 - 1) / 1e18 we can not represent it as an\n // int. This happens when x >= floor(log((2**255 - 1) / 1e18) * 1e18) ~ 135.\n if iszero(slt(x, 135305999368893231589)) {\n // Store the function selector of `ExpOverflow()`.\n mstore(0x00, 0xa37bfec9)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n }\n\n // x is now in the range (-42, 136) * 1e18. Convert to (-42, 136) * 2**96\n // for more intermediate precision and a binary basis. This base conversion\n // is a multiplication by 1e18 / 2**96 = 5**18 / 2**78.\n x = (x << 78) / 5 ** 18;\n\n // Reduce range of x to (-½ ln 2, ½ ln 2) * 2**96 by factoring out powers\n // of two such that exp(x) = exp(x') * 2**k, where k is an integer.\n // Solving this gives k = round(x / log(2)) and x' = x - k * log(2).\n int256 k = ((x << 96) / 54916777467707473351141471128 + 2 ** 95) >> 96;\n x = x - k * 54916777467707473351141471128;\n\n // k is in the range [-61, 195].\n\n // Evaluate using a (6, 7)-term rational approximation.\n // p is made monic, we'll multiply by a scale factor later.\n int256 y = x + 1346386616545796478920950773328;\n y = ((y * x) >> 96) + 57155421227552351082224309758442;\n int256 p = y + x - 94201549194550492254356042504812;\n p = ((p * y) >> 96) + 28719021644029726153956944680412240;\n p = p * x + (4385272521454847904659076985693276 << 96);\n\n // We leave p in 2**192 basis so we don't need to scale it back up for the division.\n int256 q = x - 2855989394907223263936484059900;\n q = ((q * x) >> 96) + 50020603652535783019961831881945;\n q = ((q * x) >> 96) - 533845033583426703283633433725380;\n q = ((q * x) >> 96) + 3604857256930695427073651918091429;\n q = ((q * x) >> 96) - 14423608567350463180887372962807573;\n q = ((q * x) >> 96) + 26449188498355588339934803723976023;\n\n /// @solidity memory-safe-assembly\n assembly {\n // Div in assembly because solidity adds a zero check despite the unchecked.\n // The q polynomial won't have zeros in the domain as all its roots are complex.\n // No scaling is necessary because p is already 2**96 too large.\n r := sdiv(p, q)\n }\n\n // r should be in the range (0.09, 0.25) * 2**96.\n\n // We now need to multiply r by:\n // * the scale factor s = ~6.031367120.\n // * the 2**k factor from the range reduction.\n // * the 1e18 / 2**96 factor for base conversion.\n // We do this all at once, with an intermediate result in 2**213\n // basis, so the final right shift is always by a positive amount.\n r = int256(\n (uint256(r) * 3822833074963236453042738258902158003155416615667) >> uint256(195 - k)\n );\n }\n }\n\n /// @dev Returns `ln(x)`, denominated in `WAD`.\n function lnWad(int256 x) internal pure returns (int256 r) {\n unchecked {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(sgt(x, 0)) {\n // Store the function selector of `LnWadUndefined()`.\n mstore(0x00, 0x1615e638)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n }\n\n // We want to convert x from 10**18 fixed point to 2**96 fixed point.\n // We do this by multiplying by 2**96 / 10**18. But since\n // ln(x * C) = ln(x) + ln(C), we can simply do nothing here\n // and add ln(2**96 / 10**18) at the end.\n\n // Compute k = log2(x) - 96.\n int256 k;\n /// @solidity memory-safe-assembly\n assembly {\n let v := x\n k := shl(7, lt(0xffffffffffffffffffffffffffffffff, v))\n k := or(k, shl(6, lt(0xffffffffffffffff, shr(k, v))))\n k := or(k, shl(5, lt(0xffffffff, shr(k, v))))\n\n // For the remaining 32 bits, use a De Bruijn lookup.\n // See: https://graphics.stanford.edu/~seander/bithacks.html\n v := shr(k, v)\n v := or(v, shr(1, v))\n v := or(v, shr(2, v))\n v := or(v, shr(4, v))\n v := or(v, shr(8, v))\n v := or(v, shr(16, v))\n\n // forgefmt: disable-next-item\n k := sub(or(k, byte(shr(251, mul(v, shl(224, 0x07c4acdd))),\n 0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f)), 96)\n }\n\n // Reduce range of x to (1, 2) * 2**96\n // ln(2^k * x) = k * ln(2) + ln(x)\n x <<= uint256(159 - k);\n x = int256(uint256(x) >> 159);\n\n // Evaluate using a (8, 8)-term rational approximation.\n // p is made monic, we will multiply by a scale factor later.\n int256 p = x + 3273285459638523848632254066296;\n p = ((p * x) >> 96) + 24828157081833163892658089445524;\n p = ((p * x) >> 96) + 43456485725739037958740375743393;\n p = ((p * x) >> 96) - 11111509109440967052023855526967;\n p = ((p * x) >> 96) - 45023709667254063763336534515857;\n p = ((p * x) >> 96) - 14706773417378608786704636184526;\n p = p * x - (795164235651350426258249787498 << 96);\n\n // We leave p in 2**192 basis so we don't need to scale it back up for the division.\n // q is monic by convention.\n int256 q = x + 5573035233440673466300451813936;\n q = ((q * x) >> 96) + 71694874799317883764090561454958;\n q = ((q * x) >> 96) + 283447036172924575727196451306956;\n q = ((q * x) >> 96) + 401686690394027663651624208769553;\n q = ((q * x) >> 96) + 204048457590392012362485061816622;\n q = ((q * x) >> 96) + 31853899698501571402653359427138;\n q = ((q * x) >> 96) + 909429971244387300277376558375;\n /// @solidity memory-safe-assembly\n assembly {\n // Div in assembly because solidity adds a zero check despite the unchecked.\n // The q polynomial is known not to have zeros in the domain.\n // No scaling required because p is already 2**96 too large.\n r := sdiv(p, q)\n }\n\n // r is in the range (0, 0.125) * 2**96\n\n // Finalization, we need to:\n // * multiply by the scale factor s = 5.549…\n // * add ln(2**96 / 10**18)\n // * add k * ln(2)\n // * multiply by 10**18 / 2**96 = 5**18 >> 78\n\n // mul s * 5e18 * 2**96, base is now 5**18 * 2**192\n r *= 1677202110996718588342820967067443963516166;\n // add ln(2) * k * 5e18 * 2**192\n r += 16597577552685614221487285958193947469193820559219878177908093499208371 * k;\n // add ln(2**96 / 10**18) * 5e18 * 2**192\n r += 600920179829731861736702779321621459595472258049074101567377883020018308;\n // base conversion: mul 2**18 / 2**192\n r >>= 174;\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* GENERAL NUMBER UTILITIES */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Calculates `floor(a * b / d)` with full precision.\n /// Throws if result overflows a uint256 or when `d` is zero.\n /// Credit to Remco Bloemen under MIT license: https://2π.com/21/muldiv\n function fullMulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 result) {\n /// @solidity memory-safe-assembly\n assembly {\n // forgefmt: disable-next-item\n for {} 1 {} {\n // 512-bit multiply `[prod1 prod0] = x * y`.\n // Compute the product mod `2**256` and mod `2**256 - 1`\n // then use the Chinese Remainder Theorem to reconstruct\n // the 512 bit result. The result is stored in two 256\n // variables such that `product = prod1 * 2**256 + prod0`.\n\n // Least significant 256 bits of the product.\n let prod0 := mul(x, y)\n let mm := mulmod(x, y, not(0))\n // Most significant 256 bits of the product.\n let prod1 := sub(mm, add(prod0, lt(mm, prod0)))\n\n // Handle non-overflow cases, 256 by 256 division.\n if iszero(prod1) {\n if iszero(d) {\n // Store the function selector of `FullMulDivFailed()`.\n mstore(0x00, 0xae47f702)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n result := div(prod0, d)\n break \n }\n\n // Make sure the result is less than `2**256`.\n // Also prevents `d == 0`.\n if iszero(gt(d, prod1)) {\n // Store the function selector of `FullMulDivFailed()`.\n mstore(0x00, 0xae47f702)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n ///////////////////////////////////////////////\n // 512 by 256 division.\n ///////////////////////////////////////////////\n\n // Make division exact by subtracting the remainder from `[prod1 prod0]`.\n // Compute remainder using mulmod.\n let remainder := mulmod(x, y, d)\n // Subtract 256 bit number from 512 bit number.\n prod1 := sub(prod1, gt(remainder, prod0))\n prod0 := sub(prod0, remainder)\n // Factor powers of two out of `d`.\n // Compute largest power of two divisor of `d`.\n // Always greater or equal to 1.\n let twos := and(d, sub(0, d))\n // Divide d by power of two.\n d := div(d, twos)\n // Divide [prod1 prod0] by the factors of two.\n prod0 := div(prod0, twos)\n // Shift in bits from `prod1` into `prod0`. For this we need\n // to flip `twos` such that it is `2**256 / twos`.\n // If `twos` is zero, then it becomes one.\n prod0 := or(prod0, mul(prod1, add(div(sub(0, twos), twos), 1)))\n // Invert `d mod 2**256`\n // Now that `d` is an odd number, it has an inverse\n // modulo `2**256` such that `d * inv = 1 mod 2**256`.\n // Compute the inverse by starting with a seed that is correct\n // correct for four bits. That is, `d * inv = 1 mod 2**4`.\n let inv := xor(mul(3, d), 2)\n // Now use Newton-Raphson iteration to improve the precision.\n // Thanks to Hensel's lifting lemma, this also works in modular\n // arithmetic, doubling the correct bits in each step.\n inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**8\n inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**16\n inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**32\n inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**64\n inv := mul(inv, sub(2, mul(d, inv))) // inverse mod 2**128\n result := mul(prod0, mul(inv, sub(2, mul(d, inv)))) // inverse mod 2**256\n break\n }\n }\n }\n\n /// @dev Calculates `floor(x * y / d)` with full precision, rounded up.\n /// Throws if result overflows a uint256 or when `d` is zero.\n /// Credit to Uniswap-v3-core under MIT license:\n /// https://github.com/Uniswap/v3-core/blob/contracts/libraries/FullMath.sol\n function fullMulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 result) {\n result = fullMulDiv(x, y, d);\n /// @solidity memory-safe-assembly\n assembly {\n if mulmod(x, y, d) {\n if iszero(add(result, 1)) {\n // Store the function selector of `FullMulDivFailed()`.\n mstore(0x00, 0xae47f702)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n result := add(result, 1)\n }\n }\n }\n\n /// @dev Returns `floor(x * y / d)`.\n /// Reverts if `x * y` overflows, or `d` is zero.\n function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y))\n if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {\n // Store the function selector of `MulDivFailed()`.\n mstore(0x00, 0xad251c27)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := div(mul(x, y), d)\n }\n }\n\n /// @dev Returns `ceil(x * y / d)`.\n /// Reverts if `x * y` overflows, or `d` is zero.\n function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // Equivalent to require(d != 0 && (y == 0 || x <= type(uint256).max / y))\n if iszero(mul(d, iszero(mul(y, gt(x, div(not(0), y)))))) {\n // Store the function selector of `MulDivFailed()`.\n mstore(0x00, 0xad251c27)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := add(iszero(iszero(mod(mul(x, y), d))), div(mul(x, y), d))\n }\n }\n\n /// @dev Returns `ceil(x / d)`.\n /// Reverts if `d` is zero.\n function divUp(uint256 x, uint256 d) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(d) {\n // Store the function selector of `DivFailed()`.\n mstore(0x00, 0x65244e4e)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n z := add(iszero(iszero(mod(x, d))), div(x, d))\n }\n }\n\n /// @dev Returns `max(0, x - y)`.\n function zeroFloorSub(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := mul(gt(x, y), sub(x, y))\n }\n }\n\n /// @dev Returns the square root of `x`.\n function sqrt(uint256 x) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // `floor(sqrt(2**15)) = 181`. `sqrt(2**15) - 181 = 2.84`.\n z := 181 // The \"correct\" value is 1, but this saves a multiplication later.\n\n // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad\n // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.\n\n // Let `y = x / 2**r`.\n // We check `y >= 2**(k + 8)` but shift right by `k` bits\n // each branch to ensure that if `x >= 256`, then `y >= 256`.\n let r := shl(7, lt(0xffffffffffffffffffffffffffffffffff, x))\n r := or(r, shl(6, lt(0xffffffffffffffffff, shr(r, x))))\n r := or(r, shl(5, lt(0xffffffffff, shr(r, x))))\n r := or(r, shl(4, lt(0xffffff, shr(r, x))))\n z := shl(shr(1, r), z)\n\n // Goal was to get `z*z*y` within a small factor of `x`. More iterations could\n // get y in a tighter range. Currently, we will have y in `[256, 256*(2**16))`.\n // We ensured `y >= 256` so that the relative difference between `y` and `y+1` is small.\n // That's not possible if `x < 256` but we can just verify those cases exhaustively.\n\n // Now, `z*z*y <= x < z*z*(y+1)`, and `y <= 2**(16+8)`, and either `y >= 256`, or `x < 256`.\n // Correctness can be checked exhaustively for `x < 256`, so we assume `y >= 256`.\n // Then `z*sqrt(y)` is within `sqrt(257)/sqrt(256)` of `sqrt(x)`, or about 20bps.\n\n // For `s` in the range `[1/256, 256]`, the estimate `f(s) = (181/1024) * (s+1)`\n // is in the range `(1/2.84 * sqrt(s), 2.84 * sqrt(s))`,\n // with largest error when `s = 1` and when `s = 256` or `1/256`.\n\n // Since `y` is in `[256, 256*(2**16))`, let `a = y/65536`, so that `a` is in `[1/256, 256)`.\n // Then we can estimate `sqrt(y)` using\n // `sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2**18`.\n\n // There is no overflow risk here since `y < 2**136` after the first branch above.\n z := shr(18, mul(z, add(shr(r, x), 65536))) // A `mul()` is saved from starting `z` at 181.\n\n // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.\n z := shr(1, add(z, div(x, z)))\n z := shr(1, add(z, div(x, z)))\n z := shr(1, add(z, div(x, z)))\n z := shr(1, add(z, div(x, z)))\n z := shr(1, add(z, div(x, z)))\n z := shr(1, add(z, div(x, z)))\n z := shr(1, add(z, div(x, z)))\n\n // If `x+1` is a perfect square, the Babylonian method cycles between\n // `floor(sqrt(x))` and `ceil(sqrt(x))`. This statement ensures we return floor.\n // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division\n // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.\n // If you don't care whether the floor or ceil square root is returned, you can remove this statement.\n z := sub(z, lt(div(x, z), z))\n }\n }\n\n /// @dev Returns the factorial of `x`.\n function factorial(uint256 x) internal pure returns (uint256 result) {\n /// @solidity memory-safe-assembly\n assembly {\n for {} 1 {} {\n if iszero(lt(10, x)) {\n // forgefmt: disable-next-item\n result := and(\n shr(mul(22, x), 0x375f0016260009d80004ec0002d00001e0000180000180000200000400001),\n 0x3fffff\n )\n break\n }\n if iszero(lt(57, x)) {\n let end := 31\n result := 8222838654177922817725562880000000\n if iszero(lt(end, x)) {\n end := 10\n result := 3628800\n }\n for { let w := not(0) } 1 {} {\n result := mul(result, x)\n x := add(x, w)\n if eq(x, end) { break }\n }\n break\n }\n // Store the function selector of `FactorialOverflow()`.\n mstore(0x00, 0xaba0f2a2)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n }\n }\n\n /// @dev Returns the log2 of `x`.\n /// Equivalent to computing the index of the most significant bit (MSB) of `x`.\n function log2(uint256 x) internal pure returns (uint256 r) {\n /// @solidity memory-safe-assembly\n assembly {\n if iszero(x) {\n // Store the function selector of `Log2Undefined()`.\n mstore(0x00, 0x5be3aa5c)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x))\n r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))\n r := or(r, shl(5, lt(0xffffffff, shr(r, x))))\n\n // For the remaining 32 bits, use a De Bruijn lookup.\n // See: https://graphics.stanford.edu/~seander/bithacks.html\n x := shr(r, x)\n x := or(x, shr(1, x))\n x := or(x, shr(2, x))\n x := or(x, shr(4, x))\n x := or(x, shr(8, x))\n x := or(x, shr(16, x))\n\n // forgefmt: disable-next-item\n r := or(r, byte(shr(251, mul(x, shl(224, 0x07c4acdd))),\n 0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f))\n }\n }\n\n /// @dev Returns the log2 of `x`, rounded up.\n function log2Up(uint256 x) internal pure returns (uint256 r) {\n unchecked {\n uint256 isNotPo2;\n assembly {\n isNotPo2 := iszero(iszero(and(x, sub(x, 1))))\n }\n return log2(x) + isNotPo2;\n }\n }\n\n /// @dev Returns the average of `x` and `y`.\n function avg(uint256 x, uint256 y) internal pure returns (uint256 z) {\n unchecked {\n z = (x & y) + ((x ^ y) >> 1);\n }\n }\n\n /// @dev Returns the average of `x` and `y`.\n function avg(int256 x, int256 y) internal pure returns (int256 z) {\n unchecked {\n z = (x >> 1) + (y >> 1) + (((x & 1) + (y & 1)) >> 1);\n }\n }\n\n /// @dev Returns the absolute value of `x`.\n function abs(int256 x) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n let mask := sub(0, shr(255, x))\n z := xor(mask, add(mask, x))\n }\n }\n\n /// @dev Returns the absolute distance between `x` and `y`.\n function dist(int256 x, int256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n let a := sub(y, x)\n z := xor(a, mul(xor(a, sub(x, y)), sgt(x, y)))\n }\n }\n\n /// @dev Returns the minimum of `x` and `y`.\n function min(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := xor(x, mul(xor(x, y), lt(y, x)))\n }\n }\n\n /// @dev Returns the minimum of `x` and `y`.\n function min(int256 x, int256 y) internal pure returns (int256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := xor(x, mul(xor(x, y), slt(y, x)))\n }\n }\n\n /// @dev Returns the maximum of `x` and `y`.\n function max(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := xor(x, mul(xor(x, y), gt(y, x)))\n }\n }\n\n /// @dev Returns the maximum of `x` and `y`.\n function max(int256 x, int256 y) internal pure returns (int256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := xor(x, mul(xor(x, y), sgt(y, x)))\n }\n }\n\n /// @dev Returns `x`, bounded to `minValue` and `maxValue`.\n function clamp(uint256 x, uint256 minValue, uint256 maxValue)\n internal\n pure\n returns (uint256 z)\n {\n z = min(max(x, minValue), maxValue);\n }\n\n /// @dev Returns `x`, bounded to `minValue` and `maxValue`.\n function clamp(int256 x, int256 minValue, int256 maxValue) internal pure returns (int256 z) {\n z = min(max(x, minValue), maxValue);\n }\n\n /// @dev Returns greatest common divisor of `x` and `y`.\n function gcd(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n // forgefmt: disable-next-item\n for { z := x } y {} {\n let t := y\n y := mod(z, y)\n z := t\n }\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* RAW NUMBER OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Returns `x + y`, without checking for overflow.\n function rawAdd(uint256 x, uint256 y) internal pure returns (uint256 z) {\n unchecked {\n z = x + y;\n }\n }\n\n /// @dev Returns `x + y`, without checking for overflow.\n function rawAdd(int256 x, int256 y) internal pure returns (int256 z) {\n unchecked {\n z = x + y;\n }\n }\n\n /// @dev Returns `x - y`, without checking for underflow.\n function rawSub(uint256 x, uint256 y) internal pure returns (uint256 z) {\n unchecked {\n z = x - y;\n }\n }\n\n /// @dev Returns `x - y`, without checking for underflow.\n function rawSub(int256 x, int256 y) internal pure returns (int256 z) {\n unchecked {\n z = x - y;\n }\n }\n\n /// @dev Returns `x * y`, without checking for overflow.\n function rawMul(uint256 x, uint256 y) internal pure returns (uint256 z) {\n unchecked {\n z = x * y;\n }\n }\n\n /// @dev Returns `x * y`, without checking for overflow.\n function rawMul(int256 x, int256 y) internal pure returns (int256 z) {\n unchecked {\n z = x * y;\n }\n }\n\n /// @dev Returns `x / y`, returning 0 if `y` is zero.\n function rawDiv(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := div(x, y)\n }\n }\n\n /// @dev Returns `x / y`, returning 0 if `y` is zero.\n function rawSDiv(int256 x, int256 y) internal pure returns (int256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := sdiv(x, y)\n }\n }\n\n /// @dev Returns `x % y`, returning 0 if `y` is zero.\n function rawMod(uint256 x, uint256 y) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := mod(x, y)\n }\n }\n\n /// @dev Returns `x % y`, returning 0 if `y` is zero.\n function rawSMod(int256 x, int256 y) internal pure returns (int256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := smod(x, y)\n }\n }\n\n /// @dev Returns `(x + y) % d`, return 0 if `d` if zero.\n function rawAddMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := addmod(x, y, d)\n }\n }\n\n /// @dev Returns `(x * y) % d`, return 0 if `d` if zero.\n function rawMulMod(uint256 x, uint256 y, uint256 d) internal pure returns (uint256 z) {\n /// @solidity memory-safe-assembly\n assembly {\n z := mulmod(x, y, d)\n }\n }\n}\n"},"lib/solady/src/utils/MerkleProofLib.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice Gas optimized verification of proof of inclusion for a leaf in a Merkle tree.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/MerkleProofLib.sol)\n/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/MerkleProofLib.sol)\n/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/MerkleProof.sol)\nlibrary MerkleProofLib {\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* MERKLE PROOF VERIFICATION OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Returns whether `leaf` exists in the Merkle tree with `root`, given `proof`.\n function verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf)\n internal\n pure\n returns (bool isValid)\n {\n /// @solidity memory-safe-assembly\n assembly {\n if proof.length {\n // Left shift by 5 is equivalent to multiplying by 0x20.\n let end := add(proof.offset, shl(5, proof.length))\n // Initialize `offset` to the offset of `proof` in the calldata.\n let offset := proof.offset\n // Iterate over proof elements to compute root hash.\n for {} 1 {} {\n // Slot of `leaf` in scratch space.\n // If the condition is true: 0x20, otherwise: 0x00.\n let scratch := shl(5, gt(leaf, calldataload(offset)))\n // Store elements to hash contiguously in scratch space.\n // Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes.\n mstore(scratch, leaf)\n mstore(xor(scratch, 0x20), calldataload(offset))\n // Reuse `leaf` to store the hash to reduce stack operations.\n leaf := keccak256(0x00, 0x40)\n offset := add(offset, 0x20)\n if iszero(lt(offset, end)) { break }\n }\n }\n isValid := eq(leaf, root)\n }\n }\n\n /// @dev Returns whether all `leafs` exist in the Merkle tree with `root`,\n /// given `proof` and `flags`.\n function verifyMultiProof(\n bytes32[] calldata proof,\n bytes32 root,\n bytes32[] calldata leafs,\n bool[] calldata flags\n ) internal pure returns (bool isValid) {\n // Rebuilds the root by consuming and producing values on a queue.\n // The queue starts with the `leafs` array, and goes into a `hashes` array.\n // After the process, the last element on the queue is verified\n // to be equal to the `root`.\n //\n // The `flags` array denotes whether the sibling\n // should be popped from the queue (`flag == true`), or\n // should be popped from the `proof` (`flag == false`).\n /// @solidity memory-safe-assembly\n assembly {\n // If the number of flags is correct.\n for {} eq(add(leafs.length, proof.length), add(flags.length, 1)) {} {\n // For the case where `proof.length + leafs.length == 1`.\n if iszero(flags.length) {\n // `isValid = (proof.length == 1 ? proof[0] : leafs[0]) == root`.\n // forgefmt: disable-next-item\n isValid := eq(\n calldataload(\n xor(leafs.offset, mul(xor(proof.offset, leafs.offset), proof.length))\n ),\n root\n )\n break\n }\n\n // We can use the free memory space for the queue.\n // We don't need to allocate, since the queue is temporary.\n let hashesFront := mload(0x40)\n // Copy the leafs into the hashes.\n // Sometimes, a little memory expansion costs less than branching.\n // Should cost less, even with a high free memory offset of 0x7d00.\n // Left shift by 5 is equivalent to multiplying by 0x20.\n calldatacopy(hashesFront, leafs.offset, shl(5, leafs.length))\n // Compute the back of the hashes.\n let hashesBack := add(hashesFront, shl(5, leafs.length))\n // This is the end of the memory for the queue.\n // We recycle `flags.length` to save on stack variables\n // (this trick may not always save gas).\n flags.length := add(hashesBack, shl(5, flags.length))\n\n // We don't need to make a copy of `proof.offset` or `flags.offset`,\n // as they are pass-by-value (this trick may not always save gas).\n\n for {} 1 {} {\n // Pop from `hashes`.\n let a := mload(hashesFront)\n // Pop from `hashes`.\n let b := mload(add(hashesFront, 0x20))\n hashesFront := add(hashesFront, 0x40)\n\n // If the flag is false, load the next proof,\n // else, pops from the queue.\n if iszero(calldataload(flags.offset)) {\n // Loads the next proof.\n b := calldataload(proof.offset)\n proof.offset := add(proof.offset, 0x20)\n // Unpop from `hashes`.\n hashesFront := sub(hashesFront, 0x20)\n }\n\n // Advance to the next flag offset.\n flags.offset := add(flags.offset, 0x20)\n\n // Slot of `a` in scratch space.\n // If the condition is true: 0x20, otherwise: 0x00.\n let scratch := shl(5, gt(a, b))\n // Hash the scratch space and push the result onto the queue.\n mstore(scratch, a)\n mstore(xor(scratch, 0x20), b)\n mstore(hashesBack, keccak256(0x00, 0x40))\n hashesBack := add(hashesBack, 0x20)\n if iszero(lt(hashesBack, flags.length)) { break }\n }\n // Checks if the last value in the queue is same as the root.\n isValid := eq(mload(sub(hashesBack, 0x20)), root)\n break\n }\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* EMPTY CALLDATA HELPERS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Returns an empty calldata bytes32 array.\n function emptyProof() internal pure returns (bytes32[] calldata proof) {\n /// @solidity memory-safe-assembly\n assembly {\n proof.length := 0\n }\n }\n\n /// @dev Returns an empty calldata bytes32 array.\n function emptyLeafs() internal pure returns (bytes32[] calldata leafs) {\n /// @solidity memory-safe-assembly\n assembly {\n leafs.length := 0\n }\n }\n\n /// @dev Returns an empty calldata bool array.\n function emptyFlags() internal pure returns (bool[] calldata flags) {\n /// @solidity memory-safe-assembly\n assembly {\n flags.length := 0\n }\n }\n}\n"},"lib/solady/src/utils/SafeCastLib.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice Safe integer casting library that reverts on overflow.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeCastLib.sol)\n/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol)\nlibrary SafeCastLib {\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CUSTOM ERRORS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n error Overflow();\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* UNSIGNED INTEGER SAFE CASTING OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n function toUint8(uint256 x) internal pure returns (uint8) {\n if (x >= 1 << 8) _revertOverflow();\n return uint8(x);\n }\n\n function toUint16(uint256 x) internal pure returns (uint16) {\n if (x >= 1 << 16) _revertOverflow();\n return uint16(x);\n }\n\n function toUint24(uint256 x) internal pure returns (uint24) {\n if (x >= 1 << 24) _revertOverflow();\n return uint24(x);\n }\n\n function toUint32(uint256 x) internal pure returns (uint32) {\n if (x >= 1 << 32) _revertOverflow();\n return uint32(x);\n }\n\n function toUint40(uint256 x) internal pure returns (uint40) {\n if (x >= 1 << 40) _revertOverflow();\n return uint40(x);\n }\n\n function toUint48(uint256 x) internal pure returns (uint48) {\n if (x >= 1 << 48) _revertOverflow();\n return uint48(x);\n }\n\n function toUint56(uint256 x) internal pure returns (uint56) {\n if (x >= 1 << 56) _revertOverflow();\n return uint56(x);\n }\n\n function toUint64(uint256 x) internal pure returns (uint64) {\n if (x >= 1 << 64) _revertOverflow();\n return uint64(x);\n }\n\n function toUint72(uint256 x) internal pure returns (uint72) {\n if (x >= 1 << 72) _revertOverflow();\n return uint72(x);\n }\n\n function toUint80(uint256 x) internal pure returns (uint80) {\n if (x >= 1 << 80) _revertOverflow();\n return uint80(x);\n }\n\n function toUint88(uint256 x) internal pure returns (uint88) {\n if (x >= 1 << 88) _revertOverflow();\n return uint88(x);\n }\n\n function toUint96(uint256 x) internal pure returns (uint96) {\n if (x >= 1 << 96) _revertOverflow();\n return uint96(x);\n }\n\n function toUint104(uint256 x) internal pure returns (uint104) {\n if (x >= 1 << 104) _revertOverflow();\n return uint104(x);\n }\n\n function toUint112(uint256 x) internal pure returns (uint112) {\n if (x >= 1 << 112) _revertOverflow();\n return uint112(x);\n }\n\n function toUint120(uint256 x) internal pure returns (uint120) {\n if (x >= 1 << 120) _revertOverflow();\n return uint120(x);\n }\n\n function toUint128(uint256 x) internal pure returns (uint128) {\n if (x >= 1 << 128) _revertOverflow();\n return uint128(x);\n }\n\n function toUint136(uint256 x) internal pure returns (uint136) {\n if (x >= 1 << 136) _revertOverflow();\n return uint136(x);\n }\n\n function toUint144(uint256 x) internal pure returns (uint144) {\n if (x >= 1 << 144) _revertOverflow();\n return uint144(x);\n }\n\n function toUint152(uint256 x) internal pure returns (uint152) {\n if (x >= 1 << 152) _revertOverflow();\n return uint152(x);\n }\n\n function toUint160(uint256 x) internal pure returns (uint160) {\n if (x >= 1 << 160) _revertOverflow();\n return uint160(x);\n }\n\n function toUint168(uint256 x) internal pure returns (uint168) {\n if (x >= 1 << 168) _revertOverflow();\n return uint168(x);\n }\n\n function toUint176(uint256 x) internal pure returns (uint176) {\n if (x >= 1 << 176) _revertOverflow();\n return uint176(x);\n }\n\n function toUint184(uint256 x) internal pure returns (uint184) {\n if (x >= 1 << 184) _revertOverflow();\n return uint184(x);\n }\n\n function toUint192(uint256 x) internal pure returns (uint192) {\n if (x >= 1 << 192) _revertOverflow();\n return uint192(x);\n }\n\n function toUint200(uint256 x) internal pure returns (uint200) {\n if (x >= 1 << 200) _revertOverflow();\n return uint200(x);\n }\n\n function toUint208(uint256 x) internal pure returns (uint208) {\n if (x >= 1 << 208) _revertOverflow();\n return uint208(x);\n }\n\n function toUint216(uint256 x) internal pure returns (uint216) {\n if (x >= 1 << 216) _revertOverflow();\n return uint216(x);\n }\n\n function toUint224(uint256 x) internal pure returns (uint224) {\n if (x >= 1 << 224) _revertOverflow();\n return uint224(x);\n }\n\n function toUint232(uint256 x) internal pure returns (uint232) {\n if (x >= 1 << 232) _revertOverflow();\n return uint232(x);\n }\n\n function toUint240(uint256 x) internal pure returns (uint240) {\n if (x >= 1 << 240) _revertOverflow();\n return uint240(x);\n }\n\n function toUint248(uint256 x) internal pure returns (uint248) {\n if (x >= 1 << 248) _revertOverflow();\n return uint248(x);\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* SIGNED INTEGER SAFE CASTING OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n function toInt8(int256 x) internal pure returns (int8) {\n int8 y = int8(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt16(int256 x) internal pure returns (int16) {\n int16 y = int16(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt24(int256 x) internal pure returns (int24) {\n int24 y = int24(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt32(int256 x) internal pure returns (int32) {\n int32 y = int32(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt40(int256 x) internal pure returns (int40) {\n int40 y = int40(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt48(int256 x) internal pure returns (int48) {\n int48 y = int48(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt56(int256 x) internal pure returns (int56) {\n int56 y = int56(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt64(int256 x) internal pure returns (int64) {\n int64 y = int64(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt72(int256 x) internal pure returns (int72) {\n int72 y = int72(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt80(int256 x) internal pure returns (int80) {\n int80 y = int80(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt88(int256 x) internal pure returns (int88) {\n int88 y = int88(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt96(int256 x) internal pure returns (int96) {\n int96 y = int96(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt104(int256 x) internal pure returns (int104) {\n int104 y = int104(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt112(int256 x) internal pure returns (int112) {\n int112 y = int112(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt120(int256 x) internal pure returns (int120) {\n int120 y = int120(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt128(int256 x) internal pure returns (int128) {\n int128 y = int128(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt136(int256 x) internal pure returns (int136) {\n int136 y = int136(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt144(int256 x) internal pure returns (int144) {\n int144 y = int144(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt152(int256 x) internal pure returns (int152) {\n int152 y = int152(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt160(int256 x) internal pure returns (int160) {\n int160 y = int160(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt168(int256 x) internal pure returns (int168) {\n int168 y = int168(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt176(int256 x) internal pure returns (int176) {\n int176 y = int176(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt184(int256 x) internal pure returns (int184) {\n int184 y = int184(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt192(int256 x) internal pure returns (int192) {\n int192 y = int192(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt200(int256 x) internal pure returns (int200) {\n int200 y = int200(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt208(int256 x) internal pure returns (int208) {\n int208 y = int208(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt216(int256 x) internal pure returns (int216) {\n int216 y = int216(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt224(int256 x) internal pure returns (int224) {\n int224 y = int224(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt232(int256 x) internal pure returns (int232) {\n int232 y = int232(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt240(int256 x) internal pure returns (int240) {\n int240 y = int240(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n function toInt248(int256 x) internal pure returns (int248) {\n int248 y = int248(x);\n if (x != y) _revertOverflow();\n return y;\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* PRIVATE HELPERS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n function _revertOverflow() private pure {\n /// @solidity memory-safe-assembly\n assembly {\n // Store the function selector of `Overflow()`.\n mstore(0x00, 0x35278d12)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n }\n}\n"},"lib/solady/src/utils/SafeTransferLib.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.4;\n\n/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.\n/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)\n/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)\n/// @dev Caution! This library won't check that a token has code, responsibility is delegated to the caller.\nlibrary SafeTransferLib {\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CUSTOM ERRORS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev The ETH transfer has failed.\n error ETHTransferFailed();\n\n /// @dev The ERC20 `transferFrom` has failed.\n error TransferFromFailed();\n\n /// @dev The ERC20 `transfer` has failed.\n error TransferFailed();\n\n /// @dev The ERC20 `approve` has failed.\n error ApproveFailed();\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* CONSTANTS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Suggested gas stipend for contract receiving ETH\n /// that disallows any storage writes.\n uint256 internal constant _GAS_STIPEND_NO_STORAGE_WRITES = 2300;\n\n /// @dev Suggested gas stipend for contract receiving ETH to perform a few\n /// storage reads and writes, but low enough to prevent griefing.\n /// Multiply by a small constant (e.g. 2), if needed.\n uint256 internal constant _GAS_STIPEND_NO_GRIEF = 100000;\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* ETH OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Sends `amount` (in wei) ETH to `to`.\n /// Reverts upon failure.\n function safeTransferETH(address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n // Transfer the ETH and check if it succeeded or not.\n if iszero(call(gas(), to, amount, 0, 0, 0, 0)) {\n // Store the function selector of `ETHTransferFailed()`.\n mstore(0x00, 0xb12d13eb)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n }\n }\n\n /// @dev Force sends `amount` (in wei) ETH to `to`, with a `gasStipend`.\n /// The `gasStipend` can be set to a low enough value to prevent\n /// storage writes or gas griefing.\n ///\n /// If sending via the normal procedure fails, force sends the ETH by\n /// creating a temporary contract which uses `SELFDESTRUCT` to force send the ETH.\n ///\n /// Reverts if the current contract has insufficient balance.\n function forceSafeTransferETH(address to, uint256 amount, uint256 gasStipend) internal {\n /// @solidity memory-safe-assembly\n assembly {\n // If insufficient balance, revert.\n if lt(selfbalance(), amount) {\n // Store the function selector of `ETHTransferFailed()`.\n mstore(0x00, 0xb12d13eb)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n // Transfer the ETH and check if it succeeded or not.\n if iszero(call(gasStipend, to, amount, 0, 0, 0, 0)) {\n mstore(0x00, to) // Store the address in scratch space.\n mstore8(0x0b, 0x73) // Opcode `PUSH20`.\n mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.\n // We can directly use `SELFDESTRUCT` in the contract creation.\n // Compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758\n pop(create(amount, 0x0b, 0x16))\n }\n }\n }\n\n /// @dev Force sends `amount` (in wei) ETH to `to`, with a gas stipend\n /// equal to `_GAS_STIPEND_NO_GRIEF`. This gas stipend is a reasonable default\n /// for 99% of cases and can be overriden with the three-argument version of this\n /// function if necessary.\n ///\n /// If sending via the normal procedure fails, force sends the ETH by\n /// creating a temporary contract which uses `SELFDESTRUCT` to force send the ETH.\n ///\n /// Reverts if the current contract has insufficient balance.\n function forceSafeTransferETH(address to, uint256 amount) internal {\n // Manually inlined because the compiler doesn't inline functions with branches.\n /// @solidity memory-safe-assembly\n assembly {\n // If insufficient balance, revert.\n if lt(selfbalance(), amount) {\n // Store the function selector of `ETHTransferFailed()`.\n mstore(0x00, 0xb12d13eb)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n // Transfer the ETH and check if it succeeded or not.\n if iszero(call(_GAS_STIPEND_NO_GRIEF, to, amount, 0, 0, 0, 0)) {\n mstore(0x00, to) // Store the address in scratch space.\n mstore8(0x0b, 0x73) // Opcode `PUSH20`.\n mstore8(0x20, 0xff) // Opcode `SELFDESTRUCT`.\n // We can directly use `SELFDESTRUCT` in the contract creation.\n // Compatible with `SENDALL`: https://eips.ethereum.org/EIPS/eip-4758\n pop(create(amount, 0x0b, 0x16))\n }\n }\n }\n\n /// @dev Sends `amount` (in wei) ETH to `to`, with a `gasStipend`.\n /// The `gasStipend` can be set to a low enough value to prevent\n /// storage writes or gas griefing.\n ///\n /// Simply use `gasleft()` for `gasStipend` if you don't need a gas stipend.\n ///\n /// Note: Does NOT revert upon failure.\n /// Returns whether the transfer of ETH is successful instead.\n function trySafeTransferETH(address to, uint256 amount, uint256 gasStipend)\n internal\n returns (bool success)\n {\n /// @solidity memory-safe-assembly\n assembly {\n // Transfer the ETH and check if it succeeded or not.\n success := call(gasStipend, to, amount, 0, 0, 0, 0)\n }\n }\n\n /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/\n /* ERC20 OPERATIONS */\n /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/\n\n /// @dev Sends `amount` of ERC20 `token` from `from` to `to`.\n /// Reverts upon failure.\n ///\n /// The `from` account must have at least `amount` approved for\n /// the current contract to manage.\n function safeTransferFrom(address token, address from, address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n let m := mload(0x40) // Cache the free memory pointer.\n\n // Store the function selector of `transferFrom(address,address,uint256)`.\n mstore(0x00, 0x23b872dd)\n mstore(0x20, from) // Store the `from` argument.\n mstore(0x40, to) // Store the `to` argument.\n mstore(0x60, amount) // Store the `amount` argument.\n\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n // Set success to whether the call reverted, if not we check it either\n // returned exactly 1 (can't just be non-zero data), or had no return data.\n or(eq(mload(0x00), 1), iszero(returndatasize())),\n call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)\n )\n ) {\n // Store the function selector of `TransferFromFailed()`.\n mstore(0x00, 0x7939f424)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n mstore(0x60, 0) // Restore the zero slot to zero.\n mstore(0x40, m) // Restore the free memory pointer.\n }\n }\n\n /// @dev Sends all of ERC20 `token` from `from` to `to`.\n /// Reverts upon failure.\n ///\n /// The `from` account must have at least `amount` approved for\n /// the current contract to manage.\n function safeTransferAllFrom(address token, address from, address to)\n internal\n returns (uint256 amount)\n {\n /// @solidity memory-safe-assembly\n assembly {\n let m := mload(0x40) // Cache the free memory pointer.\n\n mstore(0x00, 0x70a08231) // Store the function selector of `balanceOf(address)`.\n mstore(0x20, from) // Store the `from` argument.\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n gt(returndatasize(), 0x1f), // At least 32 bytes returned.\n staticcall(gas(), token, 0x1c, 0x24, 0x60, 0x20)\n )\n ) {\n // Store the function selector of `TransferFromFailed()`.\n mstore(0x00, 0x7939f424)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n // Store the function selector of `transferFrom(address,address,uint256)`.\n mstore(0x00, 0x23b872dd)\n mstore(0x40, to) // Store the `to` argument.\n // The `amount` argument is already written to the memory word at 0x6a.\n amount := mload(0x60)\n\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n // Set success to whether the call reverted, if not we check it either\n // returned exactly 1 (can't just be non-zero data), or had no return data.\n or(eq(mload(0x00), 1), iszero(returndatasize())),\n call(gas(), token, 0, 0x1c, 0x64, 0x00, 0x20)\n )\n ) {\n // Store the function selector of `TransferFromFailed()`.\n mstore(0x00, 0x7939f424)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n mstore(0x60, 0) // Restore the zero slot to zero.\n mstore(0x40, m) // Restore the free memory pointer.\n }\n }\n\n /// @dev Sends `amount` of ERC20 `token` from the current contract to `to`.\n /// Reverts upon failure.\n function safeTransfer(address token, address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n mstore(0x1a, to) // Store the `to` argument.\n mstore(0x3a, amount) // Store the `amount` argument.\n // Store the function selector of `transfer(address,uint256)`,\n // left by 6 bytes (enough for 8tb of memory represented by the free memory pointer).\n // We waste 6-3 = 3 bytes to save on 6 runtime gas (PUSH1 0x224 SHL).\n mstore(0x00, 0xa9059cbb000000000000)\n\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n // Set success to whether the call reverted, if not we check it either\n // returned exactly 1 (can't just be non-zero data), or had no return data.\n or(eq(mload(0x00), 1), iszero(returndatasize())),\n call(gas(), token, 0, 0x16, 0x44, 0x00, 0x20)\n )\n ) {\n // Store the function selector of `TransferFailed()`.\n mstore(0x00, 0x90b8ec18)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n // Restore the part of the free memory pointer that was overwritten,\n // which is guaranteed to be zero, if less than 8tb of memory is used.\n mstore(0x3a, 0)\n }\n }\n\n /// @dev Sends all of ERC20 `token` from the current contract to `to`.\n /// Reverts upon failure.\n function safeTransferAll(address token, address to) internal returns (uint256 amount) {\n /// @solidity memory-safe-assembly\n assembly {\n mstore(0x00, 0x70a08231) // Store the function selector of `balanceOf(address)`.\n mstore(0x20, address()) // Store the address of the current contract.\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n gt(returndatasize(), 0x1f), // At least 32 bytes returned.\n staticcall(gas(), token, 0x1c, 0x24, 0x3a, 0x20)\n )\n ) {\n // Store the function selector of `TransferFailed()`.\n mstore(0x00, 0x90b8ec18)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n\n mstore(0x1a, to) // Store the `to` argument.\n // The `amount` argument is already written to the memory word at 0x3a.\n amount := mload(0x3a)\n // Store the function selector of `transfer(address,uint256)`,\n // left by 6 bytes (enough for 8tb of memory represented by the free memory pointer).\n // We waste 6-3 = 3 bytes to save on 6 runtime gas (PUSH1 0x224 SHL).\n mstore(0x00, 0xa9059cbb000000000000)\n\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n // Set success to whether the call reverted, if not we check it either\n // returned exactly 1 (can't just be non-zero data), or had no return data.\n or(eq(mload(0x00), 1), iszero(returndatasize())),\n call(gas(), token, 0, 0x16, 0x44, 0x00, 0x20)\n )\n ) {\n // Store the function selector of `TransferFailed()`.\n mstore(0x00, 0x90b8ec18)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n // Restore the part of the free memory pointer that was overwritten,\n // which is guaranteed to be zero, if less than 8tb of memory is used.\n mstore(0x3a, 0)\n }\n }\n\n /// @dev Sets `amount` of ERC20 `token` for `to` to manage on behalf of the current contract.\n /// Reverts upon failure.\n function safeApprove(address token, address to, uint256 amount) internal {\n /// @solidity memory-safe-assembly\n assembly {\n mstore(0x1a, to) // Store the `to` argument.\n mstore(0x3a, amount) // Store the `amount` argument.\n // Store the function selector of `approve(address,uint256)`,\n // left by 6 bytes (enough for 8tb of memory represented by the free memory pointer).\n // We waste 6-3 = 3 bytes to save on 6 runtime gas (PUSH1 0x224 SHL).\n mstore(0x00, 0x095ea7b3000000000000)\n\n if iszero(\n and( // The arguments of `and` are evaluated from right to left.\n // Set success to whether the call reverted, if not we check it either\n // returned exactly 1 (can't just be non-zero data), or had no return data.\n or(eq(mload(0x00), 1), iszero(returndatasize())),\n call(gas(), token, 0, 0x16, 0x44, 0x00, 0x20)\n )\n ) {\n // Store the function selector of `ApproveFailed()`.\n mstore(0x00, 0x3e3f8f73)\n // Revert with (offset, size).\n revert(0x1c, 0x04)\n }\n // Restore the part of the free memory pointer that was overwritten,\n // which is guaranteed to be zero, if less than 8tb of memory is used.\n mstore(0x3a, 0)\n }\n }\n\n /// @dev Returns the amount of ERC20 `token` owned by `account`.\n /// Returns zero if the `token` does not exist.\n function balanceOf(address token, address account) internal view returns (uint256 amount) {\n /// @solidity memory-safe-assembly\n assembly {\n mstore(0x00, 0x70a08231) // Store the function selector of `balanceOf(address)`.\n mstore(0x20, account) // Store the `account` argument.\n amount :=\n mul(\n mload(0x20),\n and( // The arguments of `and` are evaluated from right to left.\n gt(returndatasize(), 0x1f), // At least 32 bytes returned.\n staticcall(gas(), token, 0x1c, 0x24, 0x20, 0x20)\n )\n )\n }\n }\n}\n"}},"settings":{"remappings":["@core/=contracts/core/","@modules/=contracts/modules/","ERC721A-Upgradeable/=lib/ERC721A-Upgradeable/contracts/","chiru-labs/ERC721A-Upgradeable/=lib/ERC721A-Upgradeable/contracts/","closedsea/=lib/closedsea/src/","ds-test/=lib/forge-std/lib/ds-test/src/","erc4626-tests/=lib/closedsea/lib/openzeppelin-contracts/lib/erc4626-tests/","erc721a-upgradeable/=lib/multicaller/lib/erc721a-upgradeable/contracts/","erc721a/=lib/multicaller/lib/erc721a/contracts/","forge-std/=lib/forge-std/src/","multicaller/=lib/multicaller/src/","murky/=lib/murky/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts/","openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","openzeppelin/=lib/openzeppelin-contracts/contracts/","operator-filter-registry/=lib/closedsea/lib/operator-filter-registry/","preapprove/=lib/preapprove/src/","solady/=lib/solady/src/","solmate/=lib/solady/lib/solmate/src/"],"optimizer":{"enabled":true,"runs":1000},"metadata":{"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"":["ast"],"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata"]}},"evmVersion":"london","libraries":{}}} diff --git a/lib/ERC721A-Upgradeable b/lib/ERC721A-Upgradeable index 1aab259b..05bd2b99 160000 --- a/lib/ERC721A-Upgradeable +++ b/lib/ERC721A-Upgradeable @@ -1 +1 @@ -Subproject commit 1aab259b57601f821937fa6be9d599465669c1a4 +Subproject commit 05bd2b9993e632ff898472fb6aec6d698a4c6015 diff --git a/lib/closedsea b/lib/closedsea index ec3ba08d..f980e958 160000 --- a/lib/closedsea +++ b/lib/closedsea @@ -1 +1 @@ -Subproject commit ec3ba08da37a39fb3f057fc88d9515880dfae97f +Subproject commit f980e958076835fadd5259c3c86eb3c6f50bb33d diff --git a/lib/multicaller b/lib/multicaller new file mode 160000 index 00000000..d77e75ac --- /dev/null +++ b/lib/multicaller @@ -0,0 +1 @@ +Subproject commit d77e75acbea633a986d8150f0b117895e7934299 diff --git a/lib/solady b/lib/solady index 820649e5..d8542ec0 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 820649e5833212ffe6317d9d1b70ccfb7b69ca39 +Subproject commit d8542ec0e8e0bd4564a73b4b0178d880490a976c diff --git a/placeholder b/placeholder new file mode 100644 index 00000000..e69de29b diff --git a/remappings.txt b/remappings.txt index 4763901f..937339ec 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,5 +3,8 @@ openzeppelin-upgradeable/=./lib/openzeppelin-contracts-upgradeable/contracts/ chiru-labs/ERC721A-Upgradeable/=./lib/ERC721A-Upgradeable/contracts/ solady/=./lib/solady/src/ closedsea/=./lib/closedsea/src/ +preapprove/=./lib/preapprove/src/ +multicaller/=./lib/multicaller/src/ @core=./contracts/core @modules=./contracts/modules/ +forge-std/=./lib/forge-std/src/ diff --git a/script/js/pruneArtifacts.ts b/script/js/pruneArtifacts.ts index a4e6a6f5..13a82e15 100644 --- a/script/js/pruneArtifacts.ts +++ b/script/js/pruneArtifacts.ts @@ -12,7 +12,7 @@ export async function pruneArtifacts() { } } - const inclusionStrings = ["sound", "minter", "goldenegg"]; + const inclusionStrings = ["sound", "minter", "goldenegg", "sam"]; const exclusionStrings = ["RangeEditionMinterUpdater", "RangeEditionMinterInvariants", ".t.sol", "test", "mock"]; for await (const currentPath of walk(CONTRACT_ARTIFACTS_DIR)) { let foundMatch = false; diff --git a/script/solidity/Deploy.1.1.0.s.sol b/script/solidity/Deploy.1.2.0.s.sol similarity index 65% rename from script/solidity/Deploy.1.1.0.s.sol rename to script/solidity/Deploy.1.2.0.s.sol index f2ba9b51..779246cc 100644 --- a/script/solidity/Deploy.1.1.0.s.sol +++ b/script/solidity/Deploy.1.2.0.s.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.16; import { Script } from "forge-std/Script.sol"; import { ERC1967Proxy } from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; -import { SoundFeeRegistry } from "@core/SoundFeeRegistry.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { ISoundFeeRegistry, SoundFeeRegistry } from "@core/SoundFeeRegistry.sol"; +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"; @@ -22,10 +22,22 @@ contract Deploy is Script { function run() external { vm.startBroadcast(); + // https://etherscan.io/address/0x8f921211c9771baEb648Ac7bECB322a540298A4B#readContract + ISoundFeeRegistry soundFeeRegistry = ISoundFeeRegistry(0x8f921211c9771baEb648Ac7bECB322a540298A4B); + + // Deploy minter modules + new FixedPriceSignatureMinter(soundFeeRegistry); + new MerkleDropMinter(soundFeeRegistry); + new RangeEditionMinter(soundFeeRegistry); + new EditionMaxMinter(soundFeeRegistry); + + // If only deploying minters, we're done. + if (ONLY_MINTERS) return; + // Deploy edition implementation (& initialize it for security) - SoundEditionV1_1 editionImplementation = new SoundEditionV1_1(); + SoundEditionV1_2 editionImplementation = new SoundEditionV1_2(); editionImplementation.initialize( - "SoundEditionV1.1.0", // name + "SoundEditionV1.2.0", // name "SOUND", // symbol address(0), "baseURI", diff --git a/script/solidity/GetInterfaceId.s.sol b/script/solidity/GetInterfaceId.s.sol index 114b2f66..ff2db6c5 100644 --- a/script/solidity/GetInterfaceId.s.sol +++ b/script/solidity/GetInterfaceId.s.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.16; import { Script } from "forge-std/Script.sol"; import "forge-std/console.sol"; import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { IMinterModule } from "@core/interfaces/IMinterModule.sol"; import { IFixedPriceSignatureMinter } from "@modules/interfaces/IFixedPriceSignatureMinter.sol"; import { IMerkleDropMinter } from "@modules/interfaces/IMerkleDropMinter.sol"; @@ -18,6 +19,9 @@ contract GetInterfaceId is Script { console.log('"ISoundEditionV1": "'); console.logBytes4(type(ISoundEditionV1).interfaceId); + console.log('", "ISoundEditionV1_2": "'); + console.logBytes4(type(ISoundEditionV1_2).interfaceId); + console.log('", "IMinterModule": "'); console.logBytes4(type(IMinterModule).interfaceId); diff --git a/src/interfaceIds.ts b/src/interfaceIds.ts index 37923a5a..d1014aad 100644 --- a/src/interfaceIds.ts +++ b/src/interfaceIds.ts @@ -1,6 +1,6 @@ export const interfaceIds = { ISoundEditionV1: "0x50899e54", - ISoundEditionV1_1: "0x425aac3d", + ISoundEditionV1_2: "0xa176eca6", IMinterModule: "0x37c74bd8", IFixedPriceSignatureMinter: "0xa61bd96f", IMerkleDropMinter: "0x89691c4c", diff --git a/src/json/interfaceIds.json b/src/json/interfaceIds.json index 4c347eef..15c6ebfa 100644 --- a/src/json/interfaceIds.json +++ b/src/json/interfaceIds.json @@ -1,6 +1,6 @@ { "ISoundEditionV1": "0x50899e54", - "ISoundEditionV1_1": "0x425aac3d", + "ISoundEditionV1_2": "0xa176eca6", "IMinterModule": "0x37c74bd8", "IFixedPriceSignatureMinter": "0xa61bd96f", "IMerkleDropMinter": "0x89691c4c", diff --git a/tests/SoundFeeRegistry/SoundFeeRegistry.t.sol b/tests/SoundFeeRegistry/SoundFeeRegistry.t.sol index 0f50bf23..c6c4e19f 100644 --- a/tests/SoundFeeRegistry/SoundFeeRegistry.t.sol +++ b/tests/SoundFeeRegistry/SoundFeeRegistry.t.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.16; import "../TestConfig.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { ISoundFeeRegistry, SoundFeeRegistry } from "@core/SoundFeeRegistry.sol"; contract SoundFeeRegistryTests is TestConfig { @@ -35,7 +35,7 @@ contract SoundFeeRegistryTests is TestConfig { function test_setSoundFeeAddressRevertsForNonOwner() external { address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); feeRegistry.setSoundFeeAddress(address(10)); } @@ -61,7 +61,7 @@ contract SoundFeeRegistryTests is TestConfig { function test_setPlatformFeeBPSRevertsForNonOwner() external { address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); feeRegistry.setPlatformFeeBPS(10); } diff --git a/tests/TestConfig.sol b/tests/TestConfig.sol index 26449e57..99fc28ca 100644 --- a/tests/TestConfig.sol +++ b/tests/TestConfig.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -import { Test } from "forge-std/Test.sol"; +import "./TestPlus.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundFeeRegistry } from "@core/SoundFeeRegistry.sol"; import { IMetadataModule } from "@core/interfaces/IMetadataModule.sol"; -import { MockSoundEditionV1_1 } from "./mocks/MockSoundEditionV1_1.sol"; +import { MockSoundEditionV1_2 } from "./mocks/MockSoundEditionV1_2.sol"; -contract TestConfig is Test { +contract TestConfig is TestPlus { // From ISoundEditionVI. uint8 public constant METADATA_IS_FROZEN_FLAG = 1 << 0; uint8 public constant MINT_RANDOMNESS_ENABLED_FLAG = 1 << 1; @@ -40,7 +40,7 @@ contract TestConfig is Test { feeRegistry = new SoundFeeRegistry(SOUND_FEE_ADDRESS, PLATFORM_FEE_BPS); // Deploy SoundEdition implementation - MockSoundEditionV1_1 soundEditionImplementation = new MockSoundEditionV1_1(); + MockSoundEditionV1_2 soundEditionImplementation = new MockSoundEditionV1_2(); soundCreator = new SoundCreatorV1(address(soundEditionImplementation)); } @@ -71,7 +71,7 @@ contract TestConfig is Test { uint8 flags ) public returns (address) { bytes memory initData = abi.encodeWithSelector( - SoundEditionV1_1.initialize.selector, + SoundEditionV1_2.initialize.selector, name, symbol, metadataModule, @@ -93,9 +93,9 @@ contract TestConfig is Test { return payable(addr); } - function createGenericEdition() public returns (SoundEditionV1_1) { + function createGenericEdition() public returns (SoundEditionV1_2) { return - SoundEditionV1_1( + SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, diff --git a/tests/TestPlus.sol b/tests/TestPlus.sol new file mode 100644 index 00000000..afeb4624 --- /dev/null +++ b/tests/TestPlus.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; + +contract TestPlus is Test { + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeMemory() { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + { + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(offset, zero, calldatasize()) + + // Fill the 64 bytes of scratch space with garbage. + mstore(zero, caller()) + mstore(0x20, keccak256(offset, calldatasize())) + mstore(zero, keccak256(zero, 0x40)) + + let r0 := mload(zero) + let r1 := mload(0x20) + + let cSize := add(codesize(), iszero(codesize())) + if iszero(lt(cSize, 32)) { + cSize := sub(cSize, and(mload(0x02), 31)) + } + let start := mod(mload(0x10), cSize) + let size := mul(sub(cSize, start), gt(cSize, start)) + let times := div(0x7ffff, cSize) + if iszero(lt(times, 128)) { + times := 128 + } + + // Occasionally offset the offset by a psuedorandom large amount. + // Can't be too large, or we will easily get out-of-gas errors. + offset := add(offset, mul(iszero(and(r1, 0xf)), and(r0, 0xfffff))) + + // Fill the free memory with garbage. + // prettier-ignore + for { let w := not(0) } 1 {} { + mstore(offset, r0) + mstore(add(offset, 0x20), r1) + offset := add(offset, 0x40) + // We use codecopy instead of the identity precompile + // to avoid polluting the `forge test -vvvv` output with tons of junk. + codecopy(offset, start, size) + codecopy(add(offset, size), 0, start) + offset := add(offset, cSize) + times := add(times, w) // `sub(times, 1)`. + if iszero(times) { + break + } + } + } + } + + _; + + _checkMemory(); + } + + /// @dev Returns a psuedorandom random number from [0 .. 2**256 - 1] (inclusive). + /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. + /// e.g. `testSomething(uint256) public`. + function _random() internal returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + // This is the keccak256 of a very long string I randomly mashed on my keyboard. + let sSlot := 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee + let sValue := sload(sSlot) + + mstore(0x20, sValue) + r := keccak256(0x20, 0x40) + + // If the storage is uninitialized, initialize it to the keccak256 of the calldata. + if iszero(sValue) { + sValue := sSlot + let m := mload(0x40) + calldatacopy(m, 0, calldatasize()) + r := keccak256(m, calldatasize()) + } + sstore(sSlot, add(r, 1)) + + // Do some biased sampling for more robust tests. + // prettier-ignore + for {} 1 {} { + let d := byte(0, r) + // With a 1/256 chance, randomly set `r` to any of 0,1,2. + if iszero(d) { + r := and(r, 3) + break + } + // With a 15/256 chance, set `r` to near a random power of 2. + if iszero(gt(d, 16)) { + let u := and(0x80, r) // With a 1/2 chance, negate `r`. + let t := 1 + // If the 1st byte of `r` is not greater than 32, set `t` to `not(0)`. + if iszero(gt(byte(1, r), 32)) { + t := not(0) + } + // If the 2nd byte of `r` is not greater than 128, set `t` to `xor(sValue, r)`. + if iszero(gt(byte(2, r), 128)) { + t := xor(sValue, r) + } + // Set `r` to `t` shifted left or right by a random multiple of 8. + r := sub(shl(shl(3, and(byte(3, r), 31)), t), and(r, 3)) + // Negate `r` if `u` is zero. + if iszero(u) { + r := not(r) + } + break + } + // Otherwise, just set `r` to `xor(sValue, r)`. + r := xor(sValue, r) + break + } + } + } + + /// @dev Returns a random signer and its private key. + function _randomSigner() internal returns (address signer, uint256 privateKey) { + uint256 privateKeyMax = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140; + privateKey = _bound(_random(), 1, privateKeyMax); + signer = vm.addr(privateKey); + } + + /// @dev Rounds up the free memory pointer the the next word boundary. + /// Sometimes, some Solidity operations causes the free memory pointer to be misaligned. + function _roundUpFreeMemoryPointer() internal pure { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 twoWords = 0x40; + /// @solidity memory-safe-assembly + assembly { + mstore(twoWords, and(add(mload(twoWords), 31), not(31))) + } + } + + /// @dev Misaligns the free memory pointer. + function _misalignFreeMemoryPointer() internal pure { + uint256 twoWords = 0x40; + /// @solidity memory-safe-assembly + assembly { + let m := mload(twoWords) + m := add(m, mul(and(keccak256(0x00, twoWords), 31), iszero(and(m, 31)))) + mstore(twoWords, add(m, iszero(and(m, 31)))) + } + } + + /// @dev Check if the free memory pointer and the zero slot are not contaminated. + /// Useful for cases where these slots are used for temporary storage. + function _checkMemory() internal pure { + bool zeroSlotIsNotZero; + bool freeMemoryPointerOverflowed; + /// @solidity memory-safe-assembly + assembly { + // Test at a lower, but reasonable limit for more safety room. + if gt(mload(0x40), 0xffffffff) { + freeMemoryPointerOverflowed := 1 + } + // Check the value of the zero slot. + zeroSlotIsNotZero := mload(0x60) + } + if (freeMemoryPointerOverflowed) revert("Free memory pointer overflowed!"); + if (zeroSlotIsNotZero) revert("Zero slot is not zero!"); + } + + /// @dev Check if `s`: + /// - Has sufficient memory allocated. + /// - Is aligned to a word boundary + /// - Is zero right padded (cuz some frontends like Etherscan has issues + /// with decoding non-zero-right-padded strings) + function _checkMemory(bytes memory s) internal pure { + bool notZeroRightPadded; + bool fmpNotWordAligned; + bool insufficientMalloc; + /// @solidity memory-safe-assembly + assembly { + let length := mload(s) + let lastWord := mload(add(add(s, 0x20), and(length, not(31)))) + let remainder := and(length, 31) + if remainder { + if shl(mul(8, remainder), lastWord) { + notZeroRightPadded := 1 + } + } + // Check if the free memory pointer is a multiple of 32. + fmpNotWordAligned := and(mload(0x40), 31) + // Write some garbage to the free memory. + mstore(mload(0x40), keccak256(0x00, 0x60)) + // Check if the memory allocated is sufficient. + if length { + if gt(add(add(s, 0x20), length), mload(0x40)) { + insufficientMalloc := 1 + } + } + } + if (notZeroRightPadded) revert("Not zero right padded!"); + if (fmpNotWordAligned) revert("Free memory pointer `0x40` not 32-byte word aligned!"); + if (insufficientMalloc) revert("Insufficient memory allocation!"); + _checkMemory(); + } + + /// @dev For checking the memory allocation for string `s`. + function _checkMemory(string memory s) internal pure { + _checkMemory(bytes(s)); + } + + /// @dev Adapted from: + /// https://github.com/foundry-rs/forge-std/blob/ff4bf7db008d096ea5a657f2c20516182252a3ed/src/StdUtils.sol#L10 + /// Differentially fuzzed tested against the original implementation. + function _bound( + uint256 x, + uint256 min, + uint256 max + ) internal pure virtual returns (uint256 result) { + require(min <= max, "_bound(uint256,uint256,uint256): Max is less than min."); + + /// @solidity memory-safe-assembly + assembly { + // prettier-ignore + for {} 1 {} { + // If `x` is between `min` and `max`, return `x` directly. + // This is to ensure that dictionary values + // do not get shifted if the min is nonzero. + // More info: https://github.com/foundry-rs/forge-std/issues/188 + if iszero(or(lt(x, min), gt(x, max))) { + result := x + break + } + + let size := add(sub(max, min), 1) + if and(iszero(gt(x, 3)), gt(size, x)) { + result := add(min, x) + break + } + + let w := not(0) + if and(iszero(lt(x, sub(0, 4))), gt(size, sub(w, x))) { + result := sub(max, sub(w, x)) + break + } + + // Otherwise, wrap x into the range [min, max], + // i.e. the range is inclusive. + if iszero(lt(x, max)) { + let d := sub(x, max) + let r := mod(d, size) + if iszero(r) { + result := max + break + } + result := add(add(min, r), w) + break + } + let d := sub(min, x) + let r := mod(d, size) + if iszero(r) { + result := min + break + } + result := add(sub(max, r), 1) + break + } + } + } + + /// @dev This function will make forge's gas output display the approximate codesize of + /// the test contract as the amount of gas burnt. Useful for quick guess checking if + /// certain optimizations actually compiles to similar bytecode. + function test__codesize() external view { + /// @solidity memory-safe-assembly + assembly { + // If the caller is the contract itself (i.e. recursive call), burn all the gas. + if eq(caller(), address()) { + invalid() + } + mstore(0x00, 0xf09ff470) // Store the function selector of `test__codesize()`. + pop(staticcall(codesize(), address(), 0x1c, 0x04, 0x00, 0x00)) + } + } +} diff --git a/tests/core/SoundCreator.t.sol b/tests/core/SoundCreator.t.sol index 9b192887..03301c2b 100644 --- a/tests/core/SoundCreator.t.sol +++ b/tests/core/SoundCreator.t.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.16; import { ISoundCreatorV1 } from "@core/interfaces/ISoundCreatorV1.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol"; import { FixedPriceSignatureMinter } from "@modules/FixedPriceSignatureMinter.sol"; import { MerkleDropMinter } from "@modules/MerkleDropMinter.sol"; import { RangeEditionMinter } from "@modules/RangeEditionMinter.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../TestConfig.sol"; contract SoundCreatorTests is TestConfig { @@ -33,7 +33,7 @@ contract SoundCreatorTests is TestConfig { // Tests that the factory deploys function test_deploysSoundCreator() public { // Deploy logic contracts - SoundEditionV1_1 editionImplementation = new SoundEditionV1_1(); + SoundEditionV1_2 editionImplementation = new SoundEditionV1_2(); SoundCreatorV1 soundCreator = new SoundCreatorV1(address(editionImplementation)); assert(address(soundCreator) != address(0)); @@ -42,7 +42,7 @@ contract SoundCreatorTests is TestConfig { // Tests that the factory creates a new sound NFT function test_createSound() public { - SoundEditionV1_1 soundEdition = createGenericEdition(); + SoundEditionV1_2 soundEdition = createGenericEdition(); assert(address(soundEdition) != address(0)); assertEq(soundEdition.name(), SONG_NAME); @@ -50,7 +50,7 @@ contract SoundCreatorTests is TestConfig { } function test_createSoundSetsNameAndSymbolCorrectly(string memory name, string memory symbol) public { - SoundEditionV1_1 soundEdition = SoundEditionV1_1( + SoundEditionV1_2 soundEdition = SoundEditionV1_2( createSound( name, symbol, @@ -71,8 +71,8 @@ contract SoundCreatorTests is TestConfig { } function test_createSoundRevertsOnDoubleInitialization() public { - SoundEditionV1_1 soundEdition = createGenericEdition(); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + SoundEditionV1_2 soundEdition = createGenericEdition(); + vm.expectRevert(Ownable.Unauthorized.selector); soundEdition.initialize( SONG_NAME, SONG_SYMBOL, @@ -93,7 +93,7 @@ contract SoundCreatorTests is TestConfig { assertEq(address(soundCreator.owner()), address(this)); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); vm.prank(attacker); soundCreator.transferOwnership(attacker); } @@ -103,11 +103,11 @@ contract SoundCreatorTests is TestConfig { assertEq(address(soundCreator.owner()), address(this)); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); vm.prank(newOwner); soundCreator.completeOwnershipHandover(newOwner); - vm.expectRevert(OwnableRoles.NoHandoverRequest.selector); + vm.expectRevert(Ownable.NoHandoverRequest.selector); soundCreator.completeOwnershipHandover(newOwner); vm.prank(newOwner); @@ -131,7 +131,7 @@ contract SoundCreatorTests is TestConfig { function test_attackerCantSetNewImplementation(address attacker) public { vm.assume(attacker != address(this)); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); vm.prank(attacker); soundCreator.setEditionImplementation(address(0)); } @@ -140,7 +140,7 @@ contract SoundCreatorTests is TestConfig { vm.expectRevert(ISoundCreatorV1.ImplementationAddressCantBeZero.selector); new SoundCreatorV1(address(0)); - SoundEditionV1_1 soundEdition = createGenericEdition(); + SoundEditionV1_2 soundEdition = createGenericEdition(); SoundCreatorV1 soundCreator = new SoundCreatorV1(address(soundEdition)); vm.expectRevert(ISoundCreatorV1.ImplementationAddressCantBeZero.selector); @@ -171,7 +171,7 @@ contract SoundCreatorTests is TestConfig { } // Deploy the implementation of the edition. - SoundEditionV1_1 editionImplementation = new SoundEditionV1_1(); + SoundEditionV1_2 editionImplementation = new SoundEditionV1_2(); (address soundEditionAddress, ) = soundCreator.soundEditionAddress(address(this), salt); @@ -263,8 +263,8 @@ contract SoundCreatorTests is TestConfig { // Call the create function. (, bytes[] memory results) = _createSoundEditionWithCalls(salt, contracts, data); - // Cast it to `SoundEditionV1_1` for convenience. - SoundEditionV1_1 soundEdition = SoundEditionV1_1(soundEditionAddress); + // Cast it to `SoundEditionV1_2` for convenience. + SoundEditionV1_2 soundEdition = SoundEditionV1_2(soundEditionAddress); // Check that the `MINTER_ROLE` has been assigned properly. assertTrue(soundEdition.hasAnyRole(address(signatureMinter), editionImplementation.MINTER_ROLE())); @@ -315,7 +315,7 @@ contract SoundCreatorTests is TestConfig { function _makeInitData() internal pure returns (bytes memory) { return abi.encodeWithSelector( - SoundEditionV1_1.initialize.selector, + SoundEditionV1_2.initialize.selector, SONG_NAME, SONG_SYMBOL, METADATA_MODULE, diff --git a/tests/core/SoundEdition/metadata.t.sol b/tests/core/SoundEdition/metadata.t.sol index 5dfcff69..613e9be5 100644 --- a/tests/core/SoundEdition/metadata.t.sol +++ b/tests/core/SoundEdition/metadata.t.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.16; import { Strings } from "openzeppelin/utils/Strings.sol"; import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { IMetadataModule } from "@core/interfaces/IMetadataModule.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; -import { MockSoundEditionV1_1 } from "../../mocks/MockSoundEditionV1_1.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; +import { MockSoundEditionV1_2 } from "../../mocks/MockSoundEditionV1_2.sol"; import { MockMetadataModule } from "../../mocks/MockMetadataModule.sol"; import { TestConfig } from "../../TestConfig.sol"; @@ -17,9 +17,9 @@ contract SoundEdition_metadata is TestConfig { event ContractURISet(string _contractURI); event MetadataModuleSet(address _metadataModule); - function _createEdition() internal returns (MockSoundEditionV1_1 soundEdition) { + function _createEdition() internal returns (MockSoundEditionV1_2 soundEdition) { // deploy new sound contract - soundEdition = MockSoundEditionV1_1( + soundEdition = MockSoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -38,12 +38,12 @@ contract SoundEdition_metadata is TestConfig { function _createEditionWithMetadata() internal - returns (MockSoundEditionV1_1 soundEdition, MockMetadataModule metadataModule) + returns (MockSoundEditionV1_2 soundEdition, MockMetadataModule metadataModule) { metadataModule = new MockMetadataModule(); // deploy new sound contract - soundEdition = MockSoundEditionV1_1( + soundEdition = MockSoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -62,7 +62,7 @@ contract SoundEdition_metadata is TestConfig { // Generates tokenURI using baseURI if no metadata module is selected function test_baseURIWhenNoMetadataModule() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); // mint NFTs soundEdition.mint(2); @@ -74,14 +74,14 @@ contract SoundEdition_metadata is TestConfig { // Should successfully return contract URI for the collection function test_contractURI() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); assertEq(soundEdition.contractURI(), CONTRACT_URI); } // Generate tokenURI using the metadata module function test_metadataModule() public { - (MockSoundEditionV1_1 soundEdition, ) = _createEditionWithMetadata(); + (MockSoundEditionV1_2 soundEdition, ) = _createEditionWithMetadata(); // mint NFTs soundEdition.mint(2); @@ -92,7 +92,7 @@ contract SoundEdition_metadata is TestConfig { } function test_tokenURIRevertsWhenTokenIdDoesntExist() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); vm.expectRevert(IERC721AUpgradeable.URIQueryForNonexistentToken.selector); soundEdition.tokenURI(2); @@ -103,25 +103,25 @@ contract SoundEdition_metadata is TestConfig { // ================================ function test_setBaseURIRevertsForNonOwner() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); string memory newBaseURI = "https://abc.com/"; address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); soundEdition.setBaseURI(newBaseURI); } function test_setBaseURIRevertsWhenMetadataFrozen() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); // Freeze Metadata soundEdition.freezeMetadata(); string memory newBaseURI = "https://abc.com/"; - vm.expectRevert(ISoundEditionV1_1.MetadataIsFrozen.selector); + vm.expectRevert(ISoundEditionV1_2.MetadataIsFrozen.selector); soundEdition.setBaseURI(newBaseURI); } @@ -133,7 +133,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test owner can set base URI */ - MockSoundEditionV1_1 soundEdition1 = _createEdition(); + MockSoundEditionV1_2 soundEdition1 = _createEdition(); soundEdition1.mint(2); soundEdition1.setBaseURI(newBaseURI); @@ -143,7 +143,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test admin can set base URI */ - MockSoundEditionV1_1 soundEdition2 = _createEdition(); + MockSoundEditionV1_2 soundEdition2 = _createEdition(); soundEdition2.grantRoles(ARTIST_ADMIN, soundEdition2.ADMIN_ROLE()); soundEdition2.mint(2); @@ -155,7 +155,7 @@ contract SoundEdition_metadata is TestConfig { } function test_setBaseURIEmitsEvent() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); string memory newBaseURI = "https://abc.com/"; @@ -169,25 +169,25 @@ contract SoundEdition_metadata is TestConfig { // ================================ function test_setContractURIRevertsForNonOwner() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); string memory newContractURI = "https://abc.com/"; address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); soundEdition.setContractURI(newContractURI); } function test_setContractURIRevertsWhenMetadataFrozen() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); // Freeze Metadata soundEdition.freezeMetadata(); string memory newContractURI = "https://abc.com/"; - vm.expectRevert(ISoundEditionV1_1.MetadataIsFrozen.selector); + vm.expectRevert(ISoundEditionV1_2.MetadataIsFrozen.selector); soundEdition.setContractURI(newContractURI); } @@ -197,7 +197,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test owner can set contract URI */ - MockSoundEditionV1_1 soundEdition1 = _createEdition(); + MockSoundEditionV1_2 soundEdition1 = _createEdition(); soundEdition1.setContractURI(newContractURI); @@ -206,7 +206,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test admin can set contract URI */ - MockSoundEditionV1_1 soundEdition2 = _createEdition(); + MockSoundEditionV1_2 soundEdition2 = _createEdition(); soundEdition2.grantRoles(ARTIST_ADMIN, soundEdition2.ADMIN_ROLE()); @@ -217,7 +217,7 @@ contract SoundEdition_metadata is TestConfig { } function test_setContractURIEmitsEvent() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); string memory newContractURI = "https://abc.com/"; @@ -231,25 +231,25 @@ contract SoundEdition_metadata is TestConfig { // ================================ function test_setMetadataModuleRevertsForNonOwner() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); MockMetadataModule newMetadataModule = new MockMetadataModule(); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); soundEdition.setMetadataModule(address(newMetadataModule)); } function test_setMetadataModuleRevertsWhenMetadataFrozen() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); // Freeze Metadata soundEdition.freezeMetadata(); MockMetadataModule newMetadataModule = new MockMetadataModule(); - vm.expectRevert(ISoundEditionV1_1.MetadataIsFrozen.selector); + vm.expectRevert(ISoundEditionV1_2.MetadataIsFrozen.selector); soundEdition.setMetadataModule(address(newMetadataModule)); } @@ -260,7 +260,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test owner can set metadata module */ - MockSoundEditionV1_1 soundEdition1 = _createEdition(); + MockSoundEditionV1_2 soundEdition1 = _createEdition(); // mint NFTs soundEdition1.mint(2); @@ -273,7 +273,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test admin can set metadata module */ - MockSoundEditionV1_1 soundEdition2 = _createEdition(); + MockSoundEditionV1_2 soundEdition2 = _createEdition(); soundEdition2.grantRoles(ARTIST_ADMIN, soundEdition2.ADMIN_ROLE()); @@ -286,7 +286,7 @@ contract SoundEdition_metadata is TestConfig { } function test_setMetadataModuleEmitsEvent() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); MockMetadataModule newMetadataModule = new MockMetadataModule(); @@ -300,19 +300,19 @@ contract SoundEdition_metadata is TestConfig { // ================================ function test_freezeMetadataRevertsForNonOwner() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); soundEdition.freezeMetadata(); } function test_freezeMetadataRevertsIfAlreadyFrozen() public { - MockSoundEditionV1_1 soundEdition = _createEdition(); + MockSoundEditionV1_2 soundEdition = _createEdition(); soundEdition.freezeMetadata(); - vm.expectRevert(ISoundEditionV1_1.MetadataIsFrozen.selector); + vm.expectRevert(ISoundEditionV1_2.MetadataIsFrozen.selector); soundEdition.freezeMetadata(); } @@ -320,7 +320,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test owner can freeze metadata */ - MockSoundEditionV1_1 soundEdition1 = _createEdition(); + MockSoundEditionV1_2 soundEdition1 = _createEdition(); soundEdition1.freezeMetadata(); @@ -329,7 +329,7 @@ contract SoundEdition_metadata is TestConfig { /** * Test admin can freeze metadata */ - MockSoundEditionV1_1 soundEdition2 = _createEdition(); + MockSoundEditionV1_2 soundEdition2 = _createEdition(); soundEdition2.grantRoles(ARTIST_ADMIN, soundEdition2.ADMIN_ROLE()); @@ -340,7 +340,7 @@ contract SoundEdition_metadata is TestConfig { } function test_freezeMetadataEmitsEvent() public { - (MockSoundEditionV1_1 soundEdition, IMetadataModule metadataModule) = _createEditionWithMetadata(); + (MockSoundEditionV1_2 soundEdition, IMetadataModule metadataModule) = _createEditionWithMetadata(); vm.expectEmit(false, false, false, true); emit MetadataFrozen(metadataModule, BASE_URI, CONTRACT_URI); diff --git a/tests/core/SoundEdition/mint.t.sol b/tests/core/SoundEdition/mint.t.sol index 702c812f..b5bf6fad 100644 --- a/tests/core/SoundEdition/mint.t.sol +++ b/tests/core/SoundEdition/mint.t.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.16; import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../../TestConfig.sol"; import { stdError } from "forge-std/Test.sol"; @@ -24,9 +24,9 @@ contract SoundEdition_mint is TestConfig { vm.assume(nonAdminOrOwner != address(this)); vm.assume(nonAdminOrOwner != address(0)); - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); vm.prank(nonAdminOrOwner); edition.mint(nonAdminOrOwner, 1); @@ -35,7 +35,7 @@ contract SoundEdition_mint is TestConfig { function test_adminMintCantMintPastMax() public { uint32 editionMaxMintableUpper = 50; - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -53,13 +53,13 @@ contract SoundEdition_mint is TestConfig { edition.mint(address(this), editionMaxMintableUpper); - vm.expectRevert(abi.encodeWithSelector(ISoundEditionV1_1.ExceedsEditionAvailableSupply.selector, 0)); + vm.expectRevert(abi.encodeWithSelector(ISoundEditionV1_2.ExceedsEditionAvailableSupply.selector, 0)); edition.mint(address(this), 1); } function test_adminMintSuccess() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); // Test owner can mint to own address address owner = address(12345); @@ -113,7 +113,7 @@ contract SoundEdition_mint is TestConfig { uint256 TOKEN1_ID = 1; uint256 TOKEN2_ID = 1; - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); // Assert that the token owner can burn @@ -138,7 +138,7 @@ contract SoundEdition_mint is TestConfig { uint32 editionMaxMintableLower = 1; uint32 editionMaxMintableUpper = 3; - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -176,7 +176,7 @@ contract SoundEdition_mint is TestConfig { uint32 editionMaxMintableLower = 1; uint32 editionMaxMintableUpper = 3; - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -215,16 +215,16 @@ contract SoundEdition_mint is TestConfig { } function test_setEditionMaxMintableRangeRevertsIfNotAuthorized(address attacker) external { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); vm.assume(attacker != address(this)); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); vm.prank(attacker); edition.setEditionMaxMintableRange(0, 0); } function test_setEditionMaxMintableRangeRevertsIfValueInvalid() external { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); edition.setEditionMaxMintableRange(0, 10); @@ -238,7 +238,7 @@ contract SoundEdition_mint is TestConfig { // Attempt to increase max mintable above current max - should fail, // as we have already minted tokens. - vm.expectRevert(ISoundEditionV1_1.InvalidEditionMaxMintableRange.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidEditionMaxMintableRange.selector); edition.setEditionMaxMintableRange(0, 11); // Attempt to lower max mintable below current minted count - should set to current minted count @@ -247,12 +247,12 @@ contract SoundEdition_mint is TestConfig { assertEq(edition.editionMaxMintableUpper(), 5); // Attempt to lower again - should revert - vm.expectRevert(ISoundEditionV1_1.MintHasConcluded.selector); + vm.expectRevert(ISoundEditionV1_2.MintHasConcluded.selector); edition.setEditionMaxMintableRange(0, 4); } function test_airdropSuccess() external { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address[] memory to = new address[](3); to[0] = address(10000000); @@ -293,7 +293,7 @@ contract SoundEdition_mint is TestConfig { } function test_airdropRevertsIfExceedsEditionMaxMintable() external { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); uint32 editionMaxMintableLower = 0; uint32 editionMaxMintableUpper = 9; edition.setEditionMaxMintableRange(editionMaxMintableLower, editionMaxMintableUpper); @@ -306,7 +306,7 @@ contract SoundEdition_mint is TestConfig { uint256 quantity = 4; // Reverts if the `quantity * to.length > editionMaxMintableUpper`. vm.expectRevert( - abi.encodeWithSelector(ISoundEditionV1_1.ExceedsEditionAvailableSupply.selector, editionMaxMintableUpper) + abi.encodeWithSelector(ISoundEditionV1_2.ExceedsEditionAvailableSupply.selector, editionMaxMintableUpper) ); edition.airdrop(to, quantity); @@ -316,16 +316,16 @@ contract SoundEdition_mint is TestConfig { } function test_airdropRevertsForNoAddresses() external { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address[] memory to; - vm.expectRevert(ISoundEditionV1_1.NoAddressesToAirdrop.selector); + vm.expectRevert(ISoundEditionV1_2.NoAddressesToAirdrop.selector); edition.airdrop(to, 1); } function test_airdropSetsMintRandomness() external { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); uint256 timeThreshold = block.timestamp + 10; edition.setEditionMaxMintableRange(1, EDITION_MAX_MINTABLE); @@ -349,17 +349,17 @@ contract SoundEdition_mint is TestConfig { vm.assume(nonAdminOrOwner != address(this)); vm.assume(nonAdminOrOwner != address(0)); - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address[] memory to; vm.prank(nonAdminOrOwner); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.airdrop(to, 1); } function test_setMintRandomessEnabled(bool mintRandomnessEnabled0, bool mintRandomnessEnabled1) public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); vm.expectEmit(true, true, true, true); emit MintRandomnessEnabledSet(mintRandomnessEnabled0); @@ -380,11 +380,11 @@ contract SoundEdition_mint is TestConfig { } function test_setMintRandomessEnabledRevertsWhenThereAreMints(uint32 quantity, bool mintRandomnessEnabled) public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); edition.mint(address(this), bound(quantity, 1, 10)); - vm.expectRevert(ISoundEditionV1_1.MintsAlreadyExist.selector); + vm.expectRevert(ISoundEditionV1_2.MintsAlreadyExist.selector); edition.setMintRandomnessEnabled(mintRandomnessEnabled); } @@ -394,7 +394,7 @@ contract SoundEdition_mint is TestConfig { } function test_mintRandomessEnabledUpdatesRandomness(bool mintRandomnessEnabled) public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); uint256 timeThreshold = block.timestamp + 10; edition.setEditionMaxMintableRange(1, EDITION_MAX_MINTABLE); @@ -414,7 +414,7 @@ contract SoundEdition_mint is TestConfig { } function test_setEditionMaxMintableRangeRevertsIfMintHasConcluded() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); uint256 timeThreshold = block.timestamp + 10; edition.setEditionMaxMintableRange(1, EDITION_MAX_MINTABLE); @@ -424,12 +424,12 @@ contract SoundEdition_mint is TestConfig { edition.mint(address(this), 1); - vm.expectRevert(ISoundEditionV1_1.MintHasConcluded.selector); + vm.expectRevert(ISoundEditionV1_2.MintHasConcluded.selector); edition.setEditionMaxMintableRange(1, EDITION_MAX_MINTABLE); } function test_setEditionMaxMintableRangeRevertsIfInvalidRange() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); // We can freely set the range, as long no tokens have been minted. edition.setEditionMaxMintableRange(1, 9); @@ -441,7 +441,7 @@ contract SoundEdition_mint is TestConfig { uint32 editionMaxMintableUpper = 3; // However, we cannot the lower bound to be greater than the upper bound. - vm.expectRevert(ISoundEditionV1_1.InvalidEditionMaxMintableRange.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidEditionMaxMintableRange.selector); edition.setEditionMaxMintableRange(editionMaxMintableLower, editionMaxMintableUpper); // Change the upper bound. @@ -450,12 +450,12 @@ contract SoundEdition_mint is TestConfig { edition.mint(address(this), 1); // Checks reverts if the upper bound exceeds the previous upper bound. - vm.expectRevert(ISoundEditionV1_1.InvalidEditionMaxMintableRange.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidEditionMaxMintableRange.selector); edition.setEditionMaxMintableRange(0, editionMaxMintableUpper + 1); } function test_setEditionCutoffTimeRevertsIfMintHasConcluded() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); uint256 timeThreshold = block.timestamp + 10; edition.setEditionMaxMintableRange(1, EDITION_MAX_MINTABLE); @@ -465,34 +465,12 @@ contract SoundEdition_mint is TestConfig { edition.mint(address(this), 1); - vm.expectRevert(ISoundEditionV1_1.MintHasConcluded.selector); + vm.expectRevert(ISoundEditionV1_2.MintHasConcluded.selector); edition.setEditionCutoffTime(uint32(timeThreshold)); } - function test_mintWithQuantityOverLimitReverts() public { - SoundEditionV1_1 edition = createGenericEdition(); - uint256 limit = edition.ADDRESS_BATCH_MINT_LIMIT(); - // Minting one more than the limit will revert. - vm.expectRevert(ISoundEditionV1_1.ExceedsAddressBatchMintLimit.selector); - edition.mint(address(this), limit + 1); - // Minting right at the limit is ok. - edition.mint(address(this), limit); - } - - function test_airdropWithQuantityOverLimitReverts() public { - SoundEditionV1_1 edition = createGenericEdition(); - uint256 limit = edition.ADDRESS_BATCH_MINT_LIMIT(); - address[] memory to = new address[](1); - to[0] = address(10000000); - // Airdrop with `quantity` one more than the limit will revert. - vm.expectRevert(ISoundEditionV1_1.ExceedsAddressBatchMintLimit.selector); - edition.airdrop(to, limit + 1); - // Airdrop with `quantity` right at the limit is ok. - edition.airdrop(to, limit); - } - function test_numberMintedReturnsExpectedValue() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address owner = address(12345); edition.transferOwnership(owner); diff --git a/tests/core/SoundEdition/misc.t.sol b/tests/core/SoundEdition/misc.t.sol index 350933d6..3fdec1d3 100644 --- a/tests/core/SoundEdition/misc.t.sol +++ b/tests/core/SoundEdition/misc.t.sol @@ -1,16 +1,43 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -import { IAccessControlEnumerableUpgradeable } from "openzeppelin-upgradeable/access/IAccessControlEnumerableUpgradeable.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; +import { ISoundEditionV1_2, EditionInfo } from "@core/interfaces/ISoundEditionV1_2.sol"; import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; +import { MulticallerWithSender } from "multicaller/MulticallerWithSender.sol"; +import { Ownable } from "solady/auth/Ownable.sol"; import { IMetadataModule } from "@core/interfaces/IMetadataModule.sol"; import { TestConfig } from "../../TestConfig.sol"; +contract MulticallerWithSenderUpgradeable is MulticallerWithSender { + function initialize() external { + assembly { + sstore(0, shl(160, 1)) + } + } +} + +contract MulticallerWithSenderAttacker { + fallback() external payable { + address[] memory targets = new address[](1); + targets[0] = msg.sender; + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(ISoundEditionV1_2.setRoyalty.selector, msg.sender, uint16(12)); + + MulticallerWithSender multicallerWithSender = MulticallerWithSender( + payable(LibMulticaller.MULTICALLER_WITH_SENDER) + ); + + multicallerWithSender.aggregateWithSender(targets, data, new uint256[](1)); + } +} + /** * @dev Miscellaneous tests for SoundEdition */ @@ -32,6 +59,8 @@ contract SoundEdition_misc is TestConfig { event OperatorFilteringEnablededSet(bool operatorFilteringEnabled_); + error TransferCallerNotOwnerNorApproved(); + function test_createSoundEmitsEvent() public { vm.expectEmit(true, true, true, true); @@ -52,7 +81,7 @@ contract SoundEdition_misc is TestConfig { FLAGS ); - SoundEditionV1_1( + SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -70,8 +99,10 @@ contract SoundEdition_misc is TestConfig { } function test_supportsInterface() public { - SoundEditionV1_1 edition = createGenericEdition(); - bool supportsEditionIface = edition.supportsInterface(type(ISoundEditionV1_1).interfaceId); + SoundEditionV1_2 edition = createGenericEdition(); + bool supportsEditionIface = edition.supportsInterface(type(ISoundEditionV1_2).interfaceId); + assertTrue(supportsEditionIface); + supportsEditionIface = edition.supportsInterface(type(ISoundEditionV1_1).interfaceId); assertTrue(supportsEditionIface); supportsEditionIface = edition.supportsInterface(type(ISoundEditionV1).interfaceId); assertTrue(supportsEditionIface); @@ -80,10 +111,10 @@ contract SoundEdition_misc is TestConfig { } function test_operatorFilterer() public { - SoundEditionV1_1[2] memory editions; + SoundEditionV1_2[2] memory editions; for (uint8 i; i < 2; ++i) { - editions[i] = SoundEditionV1_1( + editions[i] = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -164,4 +195,52 @@ contract SoundEdition_misc is TestConfig { assertTrue(gasUsedForEnabled > gasUsedForDisabled); } + + function test_multicallerSupport(uint256) public { + MulticallerWithSender multicallerWithSender = MulticallerWithSender( + payable(LibMulticaller.MULTICALLER_WITH_SENDER) + ); + vm.etch(LibMulticaller.MULTICALLER_WITH_SENDER, bytes(address(new MulticallerWithSenderUpgradeable()).code)); + MulticallerWithSenderUpgradeable(payable(LibMulticaller.MULTICALLER_WITH_SENDER)).initialize(); + + SoundEditionV1_2 edition = createGenericEdition(); + + MulticallerWithSenderAttacker attacker = new MulticallerWithSenderAttacker(); + + address fundingRecipient = address(attacker); + uint16 royaltyBPS = uint16(_bound(_random(), 0, 10)); + + address[] memory targets = new address[](2); + targets[0] = address(edition); + targets[1] = address(edition); + + bytes[] memory data = new bytes[](2); + data[0] = abi.encodeWithSelector(SoundEditionV1_2.setFundingRecipient.selector, fundingRecipient); + data[1] = abi.encodeWithSelector(SoundEditionV1_2.setRoyalty.selector, royaltyBPS); + + bool isUnauthorized; + if (_random() % 16 == 0) { + vm.startPrank(address(1)); + vm.expectRevert(Ownable.Unauthorized.selector); + isUnauthorized = true; + } else if (_random() % 2 == 0) { + edition.grantRoles(address(this), edition.ADMIN_ROLE()); + edition.transferOwnership(address(1)); + } + multicallerWithSender.aggregateWithSender(targets, data, new uint256[](data.length)); + if (isUnauthorized) { + return; + } + + EditionInfo memory info = edition.editionInfo(); + + assertEq(info.royaltyBPS, royaltyBPS); + assertEq(info.fundingRecipient, fundingRecipient); + + vm.deal(address(edition), 1 ether); + + edition.withdrawETH(); + + assertEq(edition.editionInfo().royaltyBPS, royaltyBPS); + } } diff --git a/tests/core/SoundEdition/payments.t.sol b/tests/core/SoundEdition/payments.t.sol index 57122640..d2f28abe 100644 --- a/tests/core/SoundEdition/payments.t.sol +++ b/tests/core/SoundEdition/payments.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { MockERC20 } from "../../mocks/MockERC20.sol"; import { TestConfig } from "../../TestConfig.sol"; @@ -19,7 +19,7 @@ contract SoundEdition_payments is TestConfig { function test_initializeRevertsForInvalidRoyaltyBPS(uint16 royaltyBPS) public { vm.assume(royaltyBPS > MAX_BPS); - vm.expectRevert(ISoundEditionV1_1.InvalidRoyaltyBPS.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidRoyaltyBPS.selector); createSound( SONG_NAME, SONG_SYMBOL, @@ -36,7 +36,7 @@ contract SoundEdition_payments is TestConfig { } function test_initializeRevertsForInvalidFundingRecipient() public { - vm.expectRevert(ISoundEditionV1_1.InvalidFundingRecipient.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidFundingRecipient.selector); createSound( SONG_NAME, SONG_SYMBOL, @@ -53,7 +53,7 @@ contract SoundEdition_payments is TestConfig { } function test_withdrawETHSuccess() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); // mint with ETH uint256 primaryETHSales = 10 ether; @@ -79,7 +79,7 @@ contract SoundEdition_payments is TestConfig { } function test_withdrawERC20Success() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); // secondary ERC20 royalties MockERC20 tokenA = new MockERC20(); @@ -122,23 +122,23 @@ contract SoundEdition_payments is TestConfig { // ================================ function test_setFundingRecipientRevertsForNonOwner() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.setFundingRecipient(getFundedAccount(2)); } function test_setFundingRecipientRevertsForZeroAddress() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); - vm.expectRevert(ISoundEditionV1_1.InvalidFundingRecipient.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidFundingRecipient.selector); edition.setFundingRecipient(address(0)); } function test_setFundingRecipientSuccess() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address newFundingRecipient = getFundedAccount(1); edition.setFundingRecipient(newFundingRecipient); @@ -147,7 +147,7 @@ contract SoundEdition_payments is TestConfig { } function test_setFundingRecipientEmitsEvent() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address newFundingRecipient = getFundedAccount(1); @@ -161,25 +161,25 @@ contract SoundEdition_payments is TestConfig { // ================================ function test_setRoyaltyRevertsForNonOwner() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.setRoyalty(500); } function test_setRoyaltyRevertsForInvalidValue(uint16 royaltyBPS) public { vm.assume(royaltyBPS > MAX_BPS); - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); - vm.expectRevert(ISoundEditionV1_1.InvalidRoyaltyBPS.selector); + vm.expectRevert(ISoundEditionV1_2.InvalidRoyaltyBPS.selector); edition.setRoyalty(royaltyBPS); } function test_setRoyaltySuccess(uint16 royaltyBPS) public { vm.assume(royaltyBPS <= MAX_BPS); - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); edition.setRoyalty(royaltyBPS); @@ -188,7 +188,7 @@ contract SoundEdition_payments is TestConfig { function test_setRoyaltyEmitsEvent(uint16 royaltyBPS) public { vm.assume(royaltyBPS <= MAX_BPS); - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); vm.expectEmit(false, false, false, true); emit RoyaltySet(royaltyBPS); @@ -203,7 +203,7 @@ contract SoundEdition_payments is TestConfig { // avoid overflow vm.assume(salePrice < 2**128); - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); (address fundingRecipient, uint256 royaltyAmount) = edition.royaltyInfo(tokenId, salePrice); @@ -216,7 +216,7 @@ contract SoundEdition_payments is TestConfig { function test_supportsERC2981Interface() public { bytes4 _INTERFACE_ID_ERC2981 = 0x2a55205a; - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); bool supportsERC2981 = edition.supportsInterface(_INTERFACE_ID_ERC2981); assertTrue(supportsERC2981); } diff --git a/tests/core/utils/MintRandomnessLib.t.sol b/tests/core/utils/MintRandomnessLib.t.sol index 9f210670..6b47cef3 100644 --- a/tests/core/utils/MintRandomnessLib.t.sol +++ b/tests/core/utils/MintRandomnessLib.t.sol @@ -10,7 +10,7 @@ contract MintRandomnessLibTest is TestConfig { if (randomness == 0) { randomness = 1; } - uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, totalMinted, 0); + uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, totalMinted, 1, 0); assertEq(newRandomness, randomness); } @@ -24,12 +24,12 @@ contract MintRandomnessLibTest is TestConfig { uint256 maxMintable = 256; uint256 maxMintableHalf = maxMintable >> 1; for (uint256 i; i < maxMintableHalf; ++i) { - uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, i, maxMintable); + uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, i, 1, maxMintable); if (randomness != newRandomness) changesBeforeHalf++; randomness = newRandomness; } for (uint256 i = maxMintableHalf; i < maxMintable; ++i) { - uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, i, maxMintable); + uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, i, 1, maxMintable); if (randomness != newRandomness) changesAfterHalf++; randomness = newRandomness; } @@ -57,10 +57,30 @@ contract MintRandomnessLibTest is TestConfig { uint256 changes; for (uint256 t; t < 8192; ++t) { uint256 randomness = t + 1; - uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, 255, 256); + uint256 newRandomness = MintRandomnessLib.nextMintRandomness(randomness, 255, 1, 256); if (randomness != newRandomness) changes++; } assertTrue(changes != 0); } } + + function test_mintRandomnessSameWithPartitions() public { + for (uint256 t; t < 2; ++t) { + uint256 randomnessBatched = t; + uint256 randomnessSeparate = t; + uint256 maxMintable = 256; + uint256 quantity = 10; + for (uint256 i; i < quantity; ++i) { + uint256 totalMinted = i; + randomnessSeparate = MintRandomnessLib.nextMintRandomness( + randomnessSeparate, + totalMinted, + 1, + maxMintable + ); + } + randomnessBatched = MintRandomnessLib.nextMintRandomness(randomnessBatched, 0, quantity, maxMintable); + assertEq(randomnessSeparate, randomnessBatched); + } + } } diff --git a/tests/invariants/RangeEditionMinterInvariants.sol b/tests/invariants/RangeEditionMinterInvariants.sol index 6df77ab7..2bbc9cdb 100644 --- a/tests/invariants/RangeEditionMinterInvariants.sol +++ b/tests/invariants/RangeEditionMinterInvariants.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { MintInfo } from "@modules/interfaces/IRangeEditionMinter.sol"; import { RangeEditionMinter } from "@modules/RangeEditionMinter.sol"; import { BaseMinter } from "@modules/BaseMinter.sol"; @@ -11,7 +11,7 @@ import { InvariantTest } from "./InvariantTest.sol"; contract RangeEditionMinterInvariants is RangeEditionMinterTests, InvariantTest { RangeEditionMinterUpdater minterUpdater; RangeEditionMinter minter; - SoundEditionV1_1 edition; + SoundEditionV1_2 edition; function setUp() public override { super.setUp(); @@ -57,10 +57,10 @@ contract RangeEditionMinterInvariants is RangeEditionMinterTests, InvariantTest contract RangeEditionMinterUpdater { uint128 constant MINT_ID = 0; - SoundEditionV1_1 edition; + SoundEditionV1_2 edition; RangeEditionMinter minter; - constructor(SoundEditionV1_1 _edition, RangeEditionMinter _minter) { + constructor(SoundEditionV1_2 _edition, RangeEditionMinter _minter) { edition = _edition; minter = _minter; } diff --git a/tests/mocks/MockSoundEditionV1_1.sol b/tests/mocks/MockSoundEditionV1_2.sol similarity index 58% rename from tests/mocks/MockSoundEditionV1_1.sol rename to tests/mocks/MockSoundEditionV1_2.sol index 773a60b6..63189f8a 100644 --- a/tests/mocks/MockSoundEditionV1_1.sol +++ b/tests/mocks/MockSoundEditionV1_2.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; -contract MockSoundEditionV1_1 is SoundEditionV1_1 { +contract MockSoundEditionV1_2 is SoundEditionV1_2 { function mint(uint256 quantity) external payable { _mint(msg.sender, quantity); } diff --git a/tests/modules/BaseMinter.t.sol b/tests/modules/BaseMinter.t.sol index dbf4b1bf..4487ba25 100644 --- a/tests/modules/BaseMinter.t.sol +++ b/tests/modules/BaseMinter.t.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.16; import { IERC721AUpgradeable } from "chiru-labs/ERC721A-Upgradeable/IERC721AUpgradeable.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { TestConfig } from "../TestConfig.sol"; import { MockMinter, MintInfo } from "../mocks/MockMinter.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { IMinterModule } from "@core/interfaces/IMinterModule.sol"; import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; @@ -52,8 +52,8 @@ contract MintControllerBaseTests is TestConfig { minter = new MockMinter(feeRegistry); } - function _createEdition(uint32 editionMaxMintable) internal returns (SoundEditionV1_1 edition) { - edition = SoundEditionV1_1( + function _createEdition(uint32 editionMaxMintable) internal returns (SoundEditionV1_2 edition) { + edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -73,7 +73,7 @@ contract MintControllerBaseTests is TestConfig { } function test_createEditionMintRevertsIfCallerNotEditionOwnerOrAdmin() external { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); address attacker = getFundedAccount(1); vm.expectRevert(IMinterModule.Unauthorized.selector); @@ -82,7 +82,7 @@ contract MintControllerBaseTests is TestConfig { } function test_createEditionMintRevertsIfAffiliateFeeBPSTooHigh() external { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint16 affiliateFeeBPS = minter.MAX_BPS() + 1; vm.expectRevert(IMinterModule.InvalidAffiliateFeeBPS.selector); @@ -90,7 +90,7 @@ contract MintControllerBaseTests is TestConfig { } function test_createEditionMintViaOwner() external { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = 0; @@ -103,7 +103,7 @@ contract MintControllerBaseTests is TestConfig { } function test_createEditionMintViaAdmin() external { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = 0; address admin = address(1037037); @@ -118,7 +118,7 @@ contract MintControllerBaseTests is TestConfig { } function test_createEditionMintIncremenetsNextMintId() external { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint256 prevMintId = minter.nextMintId(); minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -132,7 +132,7 @@ contract MintControllerBaseTests is TestConfig { } function test_mintRevertsForUnderpaid() public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -146,7 +146,7 @@ contract MintControllerBaseTests is TestConfig { } function test_mintRefundsForOverpaid() public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -168,7 +168,7 @@ contract MintControllerBaseTests is TestConfig { } function test_mintAcceptsExactPayment() public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -190,7 +190,7 @@ contract MintControllerBaseTests is TestConfig { } function test_mintRevertsWhenPaused() public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -211,7 +211,7 @@ contract MintControllerBaseTests is TestConfig { function test_mintRevertsWithZeroQuantity() public { minter.setPrice(0); - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -221,7 +221,7 @@ contract MintControllerBaseTests is TestConfig { } function test_createEditionMintMultipleTimes() external { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); for (uint256 i; i < 3; ++i) { uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -233,7 +233,7 @@ contract MintControllerBaseTests is TestConfig { minter.setPrice(0); uint32 maxSupply = 50; - SoundEditionV1_1 edition1 = _createEdition(maxSupply); + SoundEditionV1_2 edition1 = _createEdition(maxSupply); uint128 mintId1 = minter.createEditionMint(address(edition1), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -241,7 +241,7 @@ contract MintControllerBaseTests is TestConfig { minter.mint(address(edition1), mintId1, maxSupply - 1, address(0)); // try minting 2 more - should fail and tell us there is only 1 available - vm.expectRevert(abi.encodeWithSelector(ISoundEditionV1_1.ExceedsEditionAvailableSupply.selector, 1)); + vm.expectRevert(abi.encodeWithSelector(ISoundEditionV1_2.ExceedsEditionAvailableSupply.selector, 1)); minter.mint(address(edition1), mintId1, 2, address(0)); // try minting 1 more - should succeed @@ -251,7 +251,7 @@ contract MintControllerBaseTests is TestConfig { function test_setTimeRange(address nonController) public { vm.assume(nonController != address(this)); - SoundEditionV1_1 edition = _createEdition(1); + SoundEditionV1_2 edition = _createEdition(1); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); @@ -325,14 +325,14 @@ contract MintControllerBaseTests is TestConfig { } function test_setAffiliateFee() public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint16 affiliateFeeBPS = 10; _test_setAffiliateFee(edition, mintId, affiliateFeeBPS); } function test_withdrawAffiliateFeesAccrued(uint16 affiliateFeeBPS) public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint96 price = 1 ether; @@ -360,7 +360,7 @@ contract MintControllerBaseTests is TestConfig { } function test_withdrawPlatformFeesAccrued(uint16 platformFeeBPS) public { - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); uint128 mintId = minter.createEditionMint(address(edition), START_TIME, END_TIME, AFFILIATE_FEE_BPS); uint96 price = 1 ether; @@ -402,7 +402,7 @@ contract MintControllerBaseTests is TestConfig { minter.setPrice(price); - SoundEditionV1_1 edition = _createEdition(EDITION_MAX_MINTABLE); + SoundEditionV1_2 edition = _createEdition(EDITION_MAX_MINTABLE); address affiliate = affiliateIsZeroAddress ? address(0) @@ -490,7 +490,7 @@ contract MintControllerBaseTests is TestConfig { } function _test_setAffiliateFee( - SoundEditionV1_1 edition, + SoundEditionV1_2 edition, uint128 mintId, uint16 affiliateFeeBPS ) internal returns (bool) { diff --git a/tests/modules/EditionMaxMinter.t.sol b/tests/modules/EditionMaxMinter.t.sol index 36e1f928..7589c79c 100644 --- a/tests/modules/EditionMaxMinter.t.sol +++ b/tests/modules/EditionMaxMinter.t.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.16; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { EditionMaxMinter } from "@modules/EditionMaxMinter.sol"; import { IEditionMaxMinter, MintInfo } from "@modules/interfaces/IEditionMaxMinter.sol"; @@ -63,7 +63,7 @@ contract EditionMaxMinterTests is TestConfig { function _createEditionAndMinter(uint32 _maxMintablePerAccount) internal - returns (SoundEditionV1_1 edition, EditionMaxMinter minter) + returns (SoundEditionV1_2 edition, EditionMaxMinter minter) { edition = createGenericEdition(); @@ -91,7 +91,7 @@ contract EditionMaxMinterTests is TestConfig { uint16 affiliateFeeBPS, uint32 maxMintablePerAccount ) public { - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -147,7 +147,7 @@ contract EditionMaxMinterTests is TestConfig { } function test_createEditionMintEmitsEvent() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); EditionMaxMinter minter = new EditionMaxMinter(feeRegistry); @@ -167,7 +167,7 @@ contract EditionMaxMinterTests is TestConfig { } function test_mintWhenOverMaxMintablePerAccountReverts() public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(1); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(1); vm.warp(START_TIME); address caller = getFundedAccount(1); @@ -177,7 +177,7 @@ contract EditionMaxMinterTests is TestConfig { } function test_mintWhenOverMaxMintableDueToPreviousMintedReverts() public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(3); vm.warp(START_TIME); address caller = getFundedAccount(1); @@ -196,7 +196,7 @@ contract EditionMaxMinterTests is TestConfig { function test_mintWhenMintablePerAccountIsSetAndSatisfied() public { // Set max allowed per account to 3 - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(3); address caller = getFundedAccount(1); @@ -217,7 +217,7 @@ contract EditionMaxMinterTests is TestConfig { } function test_mintUpdatesValuesAndMintsCorrectly() public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); vm.warp(START_TIME); @@ -237,7 +237,7 @@ contract EditionMaxMinterTests is TestConfig { function test_mintRevertForUnderpaid() public { uint32 quantity = 2; - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(quantity); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(quantity); vm.warp(START_TIME); @@ -254,7 +254,7 @@ contract EditionMaxMinterTests is TestConfig { } function test_mintRevertsForMintNotOpen() public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); uint32 quantity = 1; @@ -281,7 +281,7 @@ contract EditionMaxMinterTests is TestConfig { } function test_setPrice(uint96 price) public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), MINT_ID, price); @@ -292,7 +292,7 @@ contract EditionMaxMinterTests is TestConfig { function test_setMaxMintablePerAccount(uint32 maxMintablePerAccount) public { vm.assume(maxMintablePerAccount != 0); - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit MaxMintablePerAccountSet(address(edition), MINT_ID, maxMintablePerAccount); @@ -302,14 +302,14 @@ contract EditionMaxMinterTests is TestConfig { } function test_setZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectRevert(IEditionMaxMinter.MaxMintablePerAccountIsZero.selector); minter.setMaxMintablePerAccount(address(edition), MINT_ID, 0); } function test_createWithZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_1 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, EditionMaxMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectRevert(IEditionMaxMinter.MaxMintablePerAccountIsZero.selector); minter.createEditionMint(address(edition), PRICE, START_TIME, END_TIME, AFFILIATE_FEE_BPS, 0); @@ -354,7 +354,7 @@ contract EditionMaxMinterTests is TestConfig { if (editionMaxMintableLower < quantity) editionMaxMintableLower = quantity; if (editionMaxMintableUpper < quantity) editionMaxMintableUpper = quantity; - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, diff --git a/tests/modules/FixedPriceSignatureMinter.t.sol b/tests/modules/FixedPriceSignatureMinter.t.sol index 40dac548..4487931d 100644 --- a/tests/modules/FixedPriceSignatureMinter.t.sol +++ b/tests/modules/FixedPriceSignatureMinter.t.sol @@ -4,8 +4,8 @@ import { ECDSA } from "solady/utils/ECDSA.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { IMinterModule } from "@core/interfaces/IMinterModule.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_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 { FixedPriceSignatureMinter } from "@modules/FixedPriceSignatureMinter.sol"; import { IFixedPriceSignatureMinter, MintInfo } from "@modules/interfaces/IFixedPriceSignatureMinter.sol"; @@ -105,7 +105,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { return abi.encodePacked(r, s, v); } - function _createEditionAndMinter() internal returns (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) { + function _createEditionAndMinter() internal returns (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) { edition = createGenericEdition(); minter = new FixedPriceSignatureMinter(feeRegistry); @@ -124,7 +124,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_createEditionMintEmitsEvent() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); FixedPriceSignatureMinter minter = new FixedPriceSignatureMinter(feeRegistry); @@ -153,7 +153,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_createEditionMintRevertsIfSignerIsZeroAddress() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); FixedPriceSignatureMinter minter = new FixedPriceSignatureMinter(feeRegistry); @@ -171,7 +171,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintRevertsIfBuyerNotAuthorized() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 claimTicket = 0; address buyer = getFundedAccount(1); @@ -223,7 +223,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintWithUnderpaidReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = quantity; @@ -252,7 +252,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintWhenSoldOutReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 claimTicket = 0; uint32 quantity = MAX_MINTABLE + 1; @@ -305,7 +305,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintWithUnauthorizedMinterReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); address buyer = getFundedAccount(1); uint32 claimTicket = 0; @@ -347,7 +347,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintForNonExistentMintIdReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = quantity; @@ -382,7 +382,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintUpdatesValuesAndMintsCorrectly() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = quantity; @@ -420,7 +420,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_multipleMintsFromSameBuyer() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 quantity = 1; uint32 signedQuantity = 2; @@ -474,8 +474,8 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_signatureCannotBeReusedOnDifferentEditions() public { - SoundEditionV1_1 edition1 = createGenericEdition(); - SoundEditionV1_1 edition2 = createGenericEdition(); + SoundEditionV1_2 edition1 = createGenericEdition(); + SoundEditionV1_2 edition2 = createGenericEdition(); // Use the same minter for both editions FixedPriceSignatureMinter minter = new FixedPriceSignatureMinter(feeRegistry); @@ -545,7 +545,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_signatureCannotBeReusedOnDifferentMintInstances() external { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); address buyer = getFundedAccount(1); @@ -618,7 +618,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { bool[] memory expectedClaimedAndUnclaimed = new bool[](numOfTokensToBuy * 2); - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint128 mintId = minter.createEditionMint( address(edition), @@ -671,7 +671,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_setMaxMintable(uint32 maxMintable) public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); vm.expectEmit(true, true, true, true); emit MaxMintableSet(address(edition), MINT_ID, maxMintable); @@ -681,7 +681,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_setMaxMintableRevertsIfCallerNotEditionOwnerOrAdmin(uint32 maxMintable) external { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); address attacker = getFundedAccount(1); vm.expectRevert(IMinterModule.Unauthorized.selector); @@ -690,7 +690,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_setPrice(uint96 price) public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), MINT_ID, price); @@ -702,7 +702,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { function test_setSigner(address signer) public { vm.assume(signer != address(0)); - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); vm.expectEmit(true, true, true, true); emit SignerSet(address(edition), MINT_ID, signer); @@ -712,7 +712,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_setZeroSignerReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); vm.expectRevert(IFixedPriceSignatureMinter.SignerIsZeroAddress.selector); minter.setSigner(address(edition), MINT_ID, address(0)); @@ -739,7 +739,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintInfo() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); FixedPriceSignatureMinter minter = new FixedPriceSignatureMinter(feeRegistry); @@ -773,7 +773,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintWithDifferentChainIdReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 claimTicket = 0; address buyer = getFundedAccount(1); @@ -817,7 +817,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintWithMoreThanSignedQuantityReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); uint32 quantity = 2; uint32 signedQuantity = 2; @@ -863,7 +863,7 @@ contract FixedPriceSignatureMinterTests is TestConfig { } function test_mintWithInvalidAffiliateReverts() public { - (SoundEditionV1_1 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); + (SoundEditionV1_2 edition, FixedPriceSignatureMinter minter) = _createEditionAndMinter(); address buyer = getFundedAccount(1); address affiliate = getFundedAccount(2); diff --git a/tests/modules/GoldenEggMetadata.t.sol b/tests/modules/GoldenEggMetadata.t.sol index 8038e16f..a927eaab 100644 --- a/tests/modules/GoldenEggMetadata.t.sol +++ b/tests/modules/GoldenEggMetadata.t.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.16; import { Strings } from "openzeppelin/utils/Strings.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { RangeEditionMinter } from "@modules/RangeEditionMinter.sol"; import { GoldenEggMetadata } from "@modules/GoldenEggMetadata.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { Ownable, OwnableRoles } from "solady/auth/OwnableRoles.sol"; import { TestConfig } from "../TestConfig.sol"; contract GoldenEggMetadataTests is TestConfig { @@ -29,7 +29,7 @@ contract GoldenEggMetadataTests is TestConfig { function _createEdition(uint32 editionCutoffTime) internal returns ( - SoundEditionV1_1 edition, + SoundEditionV1_2 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule ) @@ -37,7 +37,7 @@ contract GoldenEggMetadataTests is TestConfig { minter = new RangeEditionMinter(feeRegistry); goldenEggModule = new GoldenEggMetadata(); - edition = SoundEditionV1_1( + edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -74,7 +74,7 @@ contract GoldenEggMetadataTests is TestConfig { GoldenEggMetadata eggModule = new GoldenEggMetadata(); - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -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_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter 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_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter 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_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -185,20 +185,20 @@ contract GoldenEggMetadataTests is TestConfig { // Test if setMintRandomnessTokenThreshold only callable by Edition's owner function test_setMintRandomnessRevertsForNonOwner() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, ) = _createEdition(CUTOFF_TIME); + (SoundEditionV1_2 edition, RangeEditionMinter minter, ) = _createEdition(CUTOFF_TIME); uint32 quantity = MAX_MINTABLE_LOWER - 1; minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.setEditionMaxMintableRange(quantity, MAX_MINTABLE_UPPER); } // Test when owner lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaOwnerSuccess() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter 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_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -251,18 +251,18 @@ contract GoldenEggMetadataTests is TestConfig { // Test if setRandomnessTimeThreshold only callable by Edition's owner function test_setRandomnessTimeThresholdRevertsForNonOwner() external { - (SoundEditionV1_1 edition, , ) = _createEdition(CUTOFF_TIME); + (SoundEditionV1_2 edition, , ) = _createEdition(CUTOFF_TIME); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.setEditionCutoffTime(666); } // Test when owner lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaOwnerSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter 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_1 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, GoldenEggMetadata goldenEggModule) = _createEdition( randomnessTimeThreshold ); diff --git a/tests/modules/MerkleDropMinter.t.sol b/tests/modules/MerkleDropMinter.t.sol index 3dd618e0..07f0fb96 100644 --- a/tests/modules/MerkleDropMinter.t.sol +++ b/tests/modules/MerkleDropMinter.t.sol @@ -4,7 +4,7 @@ import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; import { Merkle } from "murky/Merkle.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { MerkleDropMinter } from "@modules/MerkleDropMinter.sol"; import { IMerkleDropMinter, MintInfo } from "@modules/interfaces/IMerkleDropMinter.sol"; @@ -74,7 +74,7 @@ contract MerkleDropMinterTests is TestConfig { ) internal returns ( - SoundEditionV1_1 edition, + SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId ) @@ -100,7 +100,7 @@ contract MerkleDropMinterTests is TestConfig { function test_canMintMultipleTimesLessThanMaxMintablePerAccount() public { uint32 maxPerAccount = 2; - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter( + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter( 0, 6, maxPerAccount @@ -128,7 +128,7 @@ contract MerkleDropMinterTests is TestConfig { function test_cannotClaimMoreThanMaxMintablePerAccount() public { uint32 maxPerAccount = 1; uint32 requestedQuantity = 2; - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter( + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter( 0, 6, maxPerAccount @@ -146,7 +146,7 @@ contract MerkleDropMinterTests is TestConfig { uint32 maxPerAccount = 3; uint32 requestedQuantity = 3; - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter( + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter( 0, 2, maxPerAccount @@ -160,7 +160,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_cannotClaimWithInvalidProof() public { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 1, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 1, 1); bytes32[] memory proof = m.getProof(leaves, 1); vm.warp(START_TIME); @@ -171,7 +171,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_setPrice(uint96 price) public { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), mintId, price); @@ -181,7 +181,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_setMaxMintable(uint32 maxMintable) public { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit MaxMintableSet(address(edition), mintId, maxMintable); @@ -191,7 +191,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_setMaxMintableRevertsIfCallerNotEditionOwnerOrAdmin(uint32 maxMintable) external { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); address attacker = getFundedAccount(1); vm.expectRevert(IMinterModule.Unauthorized.selector); @@ -201,7 +201,7 @@ contract MerkleDropMinterTests is TestConfig { function test_setMaxMintablePerAccount(uint32 maxMintablePerAccount) public { vm.assume(maxMintablePerAccount != 0); - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit MaxMintablePerAccountSet(address(edition), mintId, maxMintablePerAccount); @@ -211,7 +211,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_setMaxMintablePerAccountRevertsIfCallerNotEditionOwnerOrAdmin(uint32 maxMintable) external { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); address attacker = getFundedAccount(1); vm.expectRevert(IMinterModule.Unauthorized.selector); @@ -220,7 +220,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_setMaxMintablePerAccountWithZeroReverts() public { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectRevert(IMerkleDropMinter.MaxMintablePerAccountIsZero.selector); minter.setMaxMintablePerAccount(address(edition), mintId, 0); @@ -228,7 +228,7 @@ contract MerkleDropMinterTests is TestConfig { function test_setMerkleRootHash(bytes32 merkleRootHash) public { vm.assume(merkleRootHash != bytes32(0)); - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectEmit(true, true, true, true); emit MerkleRootHashSet(address(edition), mintId, merkleRootHash); @@ -238,14 +238,14 @@ contract MerkleDropMinterTests is TestConfig { } function test_setEmptyMerkleRootHashReverts() public { - (SoundEditionV1_1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1); vm.expectRevert(IMerkleDropMinter.MerkleRootHashIsEmpty.selector); minter.setMerkleRootHash(address(edition), mintId, bytes32(0)); } function test_setCreateWithMerkleRootHashReverts() public { - (SoundEditionV1_1 edition, MerkleDropMinter minter, ) = _createEditionAndMinter(0, 0, 1); + (SoundEditionV1_2 edition, MerkleDropMinter minter, ) = _createEditionAndMinter(0, 0, 1); vm.expectRevert(IMerkleDropMinter.MerkleRootHashIsEmpty.selector); @@ -280,7 +280,7 @@ contract MerkleDropMinterTests is TestConfig { } function test_mintInfo() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); MerkleDropMinter minter = new MerkleDropMinter(feeRegistry); setUpMerkleTree(); diff --git a/tests/modules/MintersIntegration.t.sol b/tests/modules/MintersIntegration.t.sol index aad95c18..4abec678 100644 --- a/tests/modules/MintersIntegration.t.sol +++ b/tests/modules/MintersIntegration.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.16; import { Merkle } from "murky/Merkle.sol"; import { MerkleProof } from "openzeppelin/utils/cryptography/MerkleProof.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { MerkleDropMinter } from "@modules/MerkleDropMinter.sol"; import { RangeEditionMinter } from "@modules/RangeEditionMinter.sol"; @@ -41,7 +41,7 @@ contract MintersIntegration is TestConfig { getFundedAccount(6) // User 6 - participate in public sale ]; - SoundEditionV1_1 public edition; + SoundEditionV1_2 public edition; // Helper function to setup a MerkleTree construct function setUpMerkleTree(address[] memory accounts) public returns (Merkle, bytes32[] memory) { @@ -70,7 +70,7 @@ contract MintersIntegration is TestConfig { function test_Glasshouse() public { uint32 EDITION_MAX_MINTABLE = 1000; // Setup Glass house sound edition - edition = SoundEditionV1_1( + edition = SoundEditionV1_2( createSound( "Glass House", "GLASS", diff --git a/tests/modules/OpenGoldenEggMetadata.t.sol b/tests/modules/OpenGoldenEggMetadata.t.sol index 437c03f8..565692d9 100644 --- a/tests/modules/OpenGoldenEggMetadata.t.sol +++ b/tests/modules/OpenGoldenEggMetadata.t.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.16; import { Strings } from "openzeppelin/utils/Strings.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { RangeEditionMinter } from "@modules/RangeEditionMinter.sol"; import { IOpenGoldenEggMetadata, OpenGoldenEggMetadata } from "@modules/OpenGoldenEggMetadata.sol"; -import { ISoundEditionV1_1 } from "@core/interfaces/ISoundEditionV1_1.sol"; -import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; +import { ISoundEditionV1_2 } from "@core/interfaces/ISoundEditionV1_2.sol"; +import { Ownable } from "solady/auth/Ownable.sol"; + import { TestConfig } from "../TestConfig.sol"; contract OpenGoldenEggMetadataTests is TestConfig { @@ -30,7 +31,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { function _createEdition(uint32 editionCutoffTime) internal returns ( - SoundEditionV1_1 edition, + SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule ) @@ -38,7 +39,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { minter = new RangeEditionMinter(feeRegistry); goldenEggModule = new OpenGoldenEggMetadata(); - edition = SoundEditionV1_1( + edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -80,7 +81,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { OpenGoldenEggMetadata eggModule = new OpenGoldenEggMetadata(); - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -141,7 +142,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { tokenId = 1 + (tokenId % mintQuantity); - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -177,7 +178,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if tokenURI returns default metadata using baseURI, if auction is still active function test_getTokenURIBeforeAuctionEnded() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -193,7 +194,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if tokenURI returns goldenEgg uri, when max tokens minted function test_getTokenURIAfterMaxMinted() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -210,7 +211,7 @@ 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_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -237,20 +238,20 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if setMintRandomnessTokenThreshold only callable by Edition's owner function test_setMintRandomnessRevertsForNonOwner() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, ) = _createEdition(CUTOFF_TIME); + (SoundEditionV1_2 edition, RangeEditionMinter minter, ) = _createEdition(CUTOFF_TIME); uint32 quantity = MAX_MINTABLE_LOWER - 1; minter.mint{ value: PRICE * quantity }(address(edition), MINT_ID, quantity, address(0)); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.setEditionMaxMintableRange(quantity, MAX_MINTABLE_UPPER); } // Test when owner lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaOwnerSuccess() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -272,7 +273,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test when admin lowering mintRandomnessLockAfter for insufficient sales, it generates the golden egg function test_setMintRandomnessTokenThresholdViaAdminSuccess() external { - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( CUTOFF_TIME ); @@ -303,18 +304,18 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test if setRandomnessTimeThreshold only callable by Edition's owner function test_setRandomnessTimeThresholdRevertsForNonOwner() external { - (SoundEditionV1_1 edition, , ) = _createEdition(CUTOFF_TIME); + (SoundEditionV1_2 edition, , ) = _createEdition(CUTOFF_TIME); address caller = getFundedAccount(1); vm.prank(caller); - vm.expectRevert(OwnableRoles.Unauthorized.selector); + vm.expectRevert(Ownable.Unauthorized.selector); edition.setEditionCutoffTime(666); } // Test when owner lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaOwnerSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( randomnessTimeThreshold ); @@ -338,7 +339,7 @@ contract OpenGoldenEggMetadataTests is TestConfig { // Test when admin lowering mintRandomnessTimeThreshold, it generates the golden egg function test_setRandomnessTimeThresholdViaAdminSuccess() external { uint32 randomnessTimeThreshold = type(uint32).max; - (SoundEditionV1_1 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( + (SoundEditionV1_2 edition, RangeEditionMinter minter, OpenGoldenEggMetadata goldenEggModule) = _createEdition( randomnessTimeThreshold ); diff --git a/tests/modules/RangeEditionMinter.t.sol b/tests/modules/RangeEditionMinter.t.sol index e651c1b0..4ed90e18 100644 --- a/tests/modules/RangeEditionMinter.t.sol +++ b/tests/modules/RangeEditionMinter.t.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.16; import { IERC165 } from "openzeppelin/utils/introspection/IERC165.sol"; -import { SoundEditionV1_1 } from "@core/SoundEditionV1_1.sol"; +import { SoundEditionV1_2 } from "@core/SoundEditionV1_2.sol"; import { SoundCreatorV1 } from "@core/SoundCreatorV1.sol"; import { RangeEditionMinter } from "@modules/RangeEditionMinter.sol"; import { IRangeEditionMinter, MintInfo } from "@modules/interfaces/IRangeEditionMinter.sol"; @@ -81,7 +81,7 @@ contract RangeEditionMinterTests is TestConfig { function _createEditionAndMinter(uint32 _maxMintablePerAccount) internal - returns (SoundEditionV1_1 edition, RangeEditionMinter minter) + returns (SoundEditionV1_2 edition, RangeEditionMinter minter) { edition = createGenericEdition(); @@ -112,7 +112,7 @@ contract RangeEditionMinterTests is TestConfig { uint32 maxMintableUpper, uint32 maxMintablePerAccount ) public { - SoundEditionV1_1 edition = SoundEditionV1_1( + SoundEditionV1_2 edition = SoundEditionV1_2( createSound( SONG_NAME, SONG_SYMBOL, @@ -188,7 +188,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_createEditionMintEmitsEvent() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); RangeEditionMinter minter = new RangeEditionMinter(feeRegistry); @@ -221,7 +221,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_mintWhenOverMaxMintablePerAccountReverts() public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(1); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(1); vm.warp(START_TIME); address caller = getFundedAccount(1); @@ -231,7 +231,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_mintWhenOverMaxMintableDueToPreviousMintedReverts() public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(3); vm.warp(START_TIME); address caller = getFundedAccount(1); @@ -250,7 +250,7 @@ contract RangeEditionMinterTests is TestConfig { function test_mintWhenMintablePerAccountIsSetAndSatisfied() public { // Set max allowed per account to 3 - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(3); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(3); address caller = getFundedAccount(1); @@ -271,7 +271,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_mintUpdatesValuesAndMintsCorrectly() public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); vm.warp(START_TIME); @@ -295,7 +295,7 @@ contract RangeEditionMinterTests is TestConfig { function test_mintRevertForUnderpaid() public { uint32 quantity = 2; - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(quantity); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(quantity); vm.warp(START_TIME); @@ -312,7 +312,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_mintRevertsForMintNotOpen() public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); uint32 quantity = 1; @@ -339,7 +339,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_mintRevertsForSoldOut(uint32 quantityToBuyBeforeCutoff, uint32 quantityToBuyAfterCutoff) public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); quantityToBuyBeforeCutoff = uint32((quantityToBuyBeforeCutoff % uint256(MAX_MINTABLE_UPPER * 2)) + 1); quantityToBuyAfterCutoff = uint32((quantityToBuyAfterCutoff % uint256(MAX_MINTABLE_UPPER * 2)) + 1); @@ -382,7 +382,7 @@ contract RangeEditionMinterTests is TestConfig { function test_mintBeforeAndAfterCutoffTimeBaseCase() public { uint32 quantity = 1; - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(quantity); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(quantity); uint32 maxMintableLower = 0; uint32 maxMintableUpper = 1; minter.setMaxMintableRange(address(edition), MINT_ID, maxMintableLower, maxMintableUpper); @@ -396,7 +396,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_canSetTimeRangeBaseMinter(address nonController) public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); vm.assume(nonController != address(this)); @@ -418,7 +418,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_cannotSetInvalidTimeRangeBaseMinter(uint32 startTime, uint32 endTime) public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); // Ensure startTime cannot be after cutoff time vm.assume(startTime > CUTOFF_TIME); @@ -436,7 +436,7 @@ contract RangeEditionMinterTests is TestConfig { uint32 cutoffTime, uint32 endTime ) public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); bool hasRevert; if (!(startTime < cutoffTime && cutoffTime < endTime)) { @@ -461,7 +461,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_setMaxMintableRange(uint32 maxMintableLower, uint32 maxMintableUpper) public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); bool hasRevert; @@ -485,7 +485,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_setPrice(uint96 price) public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit PriceSet(address(edition), MINT_ID, price); @@ -496,7 +496,7 @@ contract RangeEditionMinterTests is TestConfig { function test_setMaxMintablePerAccount(uint32 maxMintablePerAccount) public { vm.assume(maxMintablePerAccount != 0); - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectEmit(true, true, true, true); emit MaxMintablePerAccountSet(address(edition), MINT_ID, maxMintablePerAccount); @@ -506,14 +506,14 @@ contract RangeEditionMinterTests is TestConfig { } function test_setZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectRevert(IRangeEditionMinter.MaxMintablePerAccountIsZero.selector); minter.setMaxMintablePerAccount(address(edition), MINT_ID, 0); } function test_createWithZeroMaxMintablePerAccountReverts() public { - (SoundEditionV1_1 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); + (SoundEditionV1_2 edition, RangeEditionMinter minter) = _createEditionAndMinter(type(uint32).max); vm.expectRevert(IRangeEditionMinter.MaxMintablePerAccountIsZero.selector); minter.createEditionMint( @@ -548,7 +548,7 @@ contract RangeEditionMinterTests is TestConfig { } function test_mintInfo() public { - SoundEditionV1_1 edition = createGenericEdition(); + SoundEditionV1_2 edition = createGenericEdition(); RangeEditionMinter minter = new RangeEditionMinter(feeRegistry); diff --git a/tests/modules/SAM.t.sol b/tests/modules/SAM.t.sol new file mode 100644 index 00000000..34756e57 --- /dev/null +++ b/tests/modules/SAM.t.sol @@ -0,0 +1,2184 @@ +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 { Ownable } from "solady/auth/Ownable.sol"; +import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; +import { LibPRNG } from "solady/utils/LibPRNG.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import { MerkleProofLib } from "solady/utils/MerkleProofLib.sol"; +import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; +import { MulticallerWithSender } from "multicaller/MulticallerWithSender.sol"; +import { IOpenGoldenEggMetadata, OpenGoldenEggMetadata } from "@modules/OpenGoldenEggMetadata.sol"; +import "../TestConfig.sol"; + +contract EvilEdition { + address public owner; + uint256 public dummy; + + constructor() { + owner = msg.sender; + } + + function mintConcluded() public pure returns (bool) { + return false; + } + + function samMint(address to, uint256 quantity) public payable returns (uint256) { + dummy = uint256(uint160(to)) | quantity; + return 1; + } + + function samBurn(address from, uint256[] memory tokenIds) public { + dummy = uint256(uint160(from)) | tokenIds.length; + } + + // The following are to allow withdrawing the golden egg fees. + + function metadataModule() public view returns (address) { + return address(this); + } + + function getGoldenEggTokenId(address) public pure returns (uint256) { + return 1; + } + + function ownerOf(uint256) public view returns (address) { + return owner; + } +} + +contract MulticallerWithSenderUpgradeable is MulticallerWithSender { + function initialize() external { + assembly { + sstore(0, shl(160, 1)) + } + } +} + +contract MulticallerWithSenderAttacker { + fallback() external payable { + address[] memory targets = new address[](1); + targets[0] = msg.sender; + + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(ISAM.setAffiliateFee.selector, msg.sender, uint16(12)); + + MulticallerWithSender multicallerWithSender = MulticallerWithSender( + payable(LibMulticaller.MULTICALLER_WITH_SENDER) + ); + + multicallerWithSender.aggregateWithSender(targets, data, new uint256[](1)); + } +} + +contract MockSAM is SAM { + bool internal _checkEdition; + + function directSetPoolBalance(address edition, uint256 balance) public { + _samData[edition].balance = SafeCastLib.toUint112(balance); + } + + function setCheckEdition(bool value) public { + _checkEdition = value; + } + + function _requireEditionIsApproved( + address edition, + address by, + bytes32 salt + ) internal view virtual override { + if (_checkEdition) { + super._requireEditionIsApproved(edition, by, salt); + } + } + + function create( + address edition, + uint96 basePrice, + uint128 linearPriceSlope, + uint128 inflectionPrice, + uint32 inflectionPoint, + uint32 maxSupply, + uint32 buyFreezeTime, + uint16 artistFeeBPS, + uint16 goldenEggFeeBPS, + uint16 affiliateFeeBPS + ) public { + super.create( + edition, + basePrice, + linearPriceSlope, + inflectionPrice, + inflectionPoint, + maxSupply, + buyFreezeTime, + artistFeeBPS, + goldenEggFeeBPS, + affiliateFeeBPS, + address(0), + bytes32(0) + ); + } + + function buy( + address edition, + address to, + uint32 quantity + ) public payable { + super.buy(edition, to, quantity, address(0), MerkleProofLib.emptyProof(), 0); + } + + function buy( + address edition, + address to, + uint32 quantity, + address affiliate, + bytes32[] calldata affiliateProof + ) public payable { + super.buy(edition, to, quantity, affiliate, affiliateProof, 0); + } + + function sell( + address edition, + uint256[] calldata tokenIds, + uint256 minimumPayout, + address payoutTo + ) external { + super.sell(edition, tokenIds, minimumPayout, payoutTo, 0); + } +} + +contract SAMTests is TestConfig { + using LibPRNG for LibPRNG.PRNG; + + uint96 constant BASE_PRICE = 0.01 ether; + + uint128 constant LINEAR_PRICE_SLOPE = 0.1 ether; + + uint128 constant INFLECTION_PRICE = 1.3 ether; + + uint32 constant INFLECTION_POINT = 50; + + uint32 constant END_TIME = 300; + + uint16 constant ARTIST_FEE_BPS = 500; + + uint16 constant AFFILIATE_FEE_BPS = 100; + + uint16 constant GOLDEN_EGG_FEE_BPS = 50; + + uint32 constant EDITION_MAX_MINTABLE_LOWER = 5; + + uint32 constant MAX_SUPPLY = 2**32 - 1; + + uint32 constant BUY_FREEZE_TIME = 2**32 - 1; + + event Created( + address indexed edition, + uint96 basePrice, + uint128 linearPriceSlope, + uint128 inflectionPrice, + uint32 inflectionPoint, + uint32 maxSupply, + uint32 buyFreezeTime, + uint16 artistFeeBPS, + uint16 goldenEggFeeBPS, + uint16 affiliateFeeBPS + ); + + 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 + ); + + event Sold( + address indexed edition, + address indexed seller, + uint32 fromCurveSupply, + uint256[] tokenIds, + uint128 totalPayout, + uint256 indexed attributionId + ); + + event BasePriceSet(address indexed edition, uint96 basePrice); + + event LinearPriceSlopeSet(address indexed edition, uint128 linearPriceSlope); + + event InflectionPriceSet(address indexed edition, uint128 inflectionPrice); + + event InflectionPointSet(address indexed edition, uint32 inflectionPoint); + + event ArtistFeeSet(address indexed edition, uint16 bps); + + event AffiliateFeeSet(address indexed edition, uint16 bps); + + event AffiliateMerkleRootSet(address indexed edition, bytes32 root); + + event GoldenEggFeeSet(address indexed edition, uint16 bps); + + event MaxSupplySet(address indexed edition, uint32 maxSupply); + + event BuyFreezeTimeSet(address indexed edition, uint32 buyFreezeTime); + + event PlatformFeeSet(uint16 bps); + + event PlatformFeeAddressSet(address addr); + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + event AffiliateFeesWithdrawn(address indexed affiliate, uint256 accrued); + + event GoldenEggFeesWithdrawn(address indexed edition, address indexed recipient, uint128 accrued); + + event PlatformFeesWithdrawn(uint128 accrued); + + event ApprovedEditionFactoriesSet(address[] factories); + + struct _testTempVariables { + uint256 basePrice; + uint256 linearPriceSlope; + uint256 inflectionPrice; + uint256 inflectionPoint; + uint256 maxSupply; + uint256 buyFreezeTime; + uint256 artistFeeBPS; + uint256 affiliateFeeBPS; + uint256 goldenEggFeeBPS; + uint256[2] totalBuyPrices; + uint256[2] quantities; + uint256[2] payments; + uint256[2] totalSellPrices; + uint256[2] payouts; + uint256[2] balancesBefore; + uint256[2] balancesAfter; + address[2] collectors; + address[2] affiliates; + uint256[][2] tokenIds; + uint256[] goldenEggIds; + uint256 totalInflows; + uint256 totalPoolValue; + uint256 totalGoldenEggFeesAccrued; + uint256 totalArtistFeesAccrued; + uint256 totalAffiliateFeesAccrued; + uint256 platformFeesAccrued; + uint256 totalFees; + uint256 platformFeeBPS; + uint256 fromTokenId; + uint256 totalFeeBPS; + uint256 feePerBPS; + uint256 artistFee; + uint256 platformFee; + uint256 goldenEggFee; + uint256 affiliateFee; + uint256 numMintedBefore; + uint256 numCollectedBefore; + uint256 numBurnedBefore; + uint256 totalSupplyBefore; + uint256 maxArtistFeeBPS; + uint256 maxAffiliateFeeBPS; + uint256 maxGoldenEggFeeBPS; + uint256 maxPlatformFeeBPS; + uint256 attributionId; + address affiliate; + } + + function test_supportsInterface() public { + MockSAM sam = new MockSAM(); + + bool supportsISAM = sam.supportsInterface(type(ISAM).interfaceId); + bool supports165 = sam.supportsInterface(type(IERC165).interfaceId); + + assertTrue(supports165); + assertTrue(supportsISAM); + } + + function _createEditionAndSAM() internal returns (SoundEditionV1_2 edition, MockSAM sam) { + edition = createGenericEdition(); + + edition.setEditionMaxMintableRange(EDITION_MAX_MINTABLE_LOWER, EDITION_MAX_MINTABLE_LOWER); + edition.setEditionCutoffTime(0); + _setOpenGoldenEggMetadataModule(edition); + + sam = new MockSAM(); + + edition.setSAM(address(sam)); + + sam.create( + address(edition), + BASE_PRICE, + LINEAR_PRICE_SLOPE, + INFLECTION_PRICE, + INFLECTION_POINT, + MAX_SUPPLY, + BUY_FREEZE_TIME, + ARTIST_FEE_BPS, + GOLDEN_EGG_FEE_BPS, + AFFILIATE_FEE_BPS + ); + } + + function _getGoldenEggId(SoundEditionV1_2 edition) internal view returns (uint256) { + return IOpenGoldenEggMetadata(edition.metadataModule()).getGoldenEggTokenId(address(edition)); + } + + function _maxMint(SoundEditionV1_2 edition) internal { + edition.mint(address(this), edition.editionMaxMintable()); + } + + function _mintOut(SoundEditionV1_2 edition) internal { + if (_random() % 2 == 0) { + _maxMint(edition); + } else if (_random() % 2 == 0) { + edition.setEditionMaxMintableRange(0, 0); + } else if (_random() % 2 == 0) { + edition.mint(address(this), 1); + edition.setEditionCutoffTime(1); + edition.setEditionMaxMintableRange(1, EDITION_MAX_MINTABLE_LOWER); + } else { + edition.setEditionCutoffTime(1); + edition.setEditionMaxMintableRange(0, EDITION_MAX_MINTABLE_LOWER); + } + } + + function _setOpenGoldenEggMetadataModule(SoundEditionV1_2 edition) internal { + edition.setMetadataModule(address(new OpenGoldenEggMetadata())); + } + + function _randomCollectors() internal returns (address[2] memory collectors) { + do { + (collectors[0], ) = _randomSigner(); + (collectors[1], ) = _randomSigner(); + if (collectors[0] == address(this) || collectors[1] == address(this)) continue; + } while (collectors[0] == collectors[1]); + + vm.deal(collectors[0], type(uint192).max); + vm.deal(collectors[1], type(uint192).max); + } + + function test_balanceExploitReverts() public { + (address exploiter, ) = _randomSigner(); + vm.startPrank(exploiter); + vm.deal(exploiter, 1); + + MockSAM sam = new MockSAM(); + EvilEdition edition = new EvilEdition(); + + vm.deal(address(sam), 1000 ether); + + sam.create( + address(edition), + 0, // Linear price slope. + 0, // Base price. + 1, // Inflection price. + type(uint32).max, // Inflection point. + MAX_SUPPLY, + BUY_FREEZE_TIME, + 1000, // Artist fee BPS. + 0, // Golden egg fee BPS. + 0 // Affiliate fee BPS. + ); + + sam.buy{ value: 1 }(address(edition), exploiter, 1, address(0), new bytes32[](0)); + + vm.expectRevert(ISAM.InSAMPhase.selector); + sam.setInflectionPrice(address(edition), 500 ether); + vm.expectRevert(ISAM.InSAMPhase.selector); + sam.setInflectionPoint(address(edition), 1); + vm.expectRevert(ISAM.InSAMPhase.selector); + sam.setBasePrice(address(edition), 100 ether); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + + vm.expectRevert(abi.encodeWithSignature("InsufficientPayout(uint256,uint256)", 0, 1 ether)); + sam.sell(address(edition), tokenIds, 1 ether, exploiter); + + sam.sell(address(edition), tokenIds, 0 ether, exploiter); + } + + function test_balanceExploitCannotWork(uint256) public { + _testTempVariables memory t; + (address exploiter, ) = _randomSigner(); + vm.startPrank(exploiter); + vm.deal(exploiter, type(uint192).max); + uint256 exploiterBalanceBefore = exploiter.balance; + + MockSAM sam = new MockSAM(); + EvilEdition edition = new EvilEdition(); + + t.maxArtistFeeBPS = sam.MAX_ARTIST_FEE_BPS(); + t.maxAffiliateFeeBPS = sam.MAX_AFFILIATE_FEE_BPS(); + t.maxGoldenEggFeeBPS = sam.MAX_GOLDEN_EGG_FEE_BPS(); + t.maxPlatformFeeBPS = sam.MAX_PLATFORM_FEE_BPS(); + + t.basePrice = _bound(_random(), 0, type(uint96).max); + t.linearPriceSlope = _bound(_random(), 1, type(uint96).max); + t.inflectionPrice = _bound(_random(), 1, type(uint96).max); + t.inflectionPoint = _bound(_random(), 1, type(uint32).max); + t.maxSupply = _bound(_random(), 0, type(uint32).max); + t.buyFreezeTime = _bound(_random(), 0, type(uint32).max); + t.artistFeeBPS = _bound(_random(), 0, t.maxArtistFeeBPS); + t.affiliateFeeBPS = _bound(_random(), 0, t.maxAffiliateFeeBPS); + t.goldenEggFeeBPS = _bound(_random(), 0, t.maxGoldenEggFeeBPS); + + sam.create( + address(edition), + uint96(t.basePrice), + uint128(t.linearPriceSlope), + uint128(t.inflectionPrice), + uint32(t.inflectionPoint), + MAX_SUPPLY, + BUY_FREEZE_TIME, + uint16(t.artistFeeBPS), + uint16(t.goldenEggFeeBPS), + uint16(t.affiliateFeeBPS) + ); + + (address affiliate, ) = _randomSigner(); + uint256 n = _bound(_random(), 1, 10); + uint256 samBaseBalance = type(uint192).max; + vm.deal(address(sam), samBaseBalance); + assertEq(address(sam).balance, samBaseBalance); + sam.buy{ value: exploiter.balance }(address(edition), exploiter, uint32(n), affiliate, new bytes32[](0)); + + try sam.setInflectionPrice(address(edition), uint96(_random())) {} catch {} + try sam.setInflectionPoint(address(edition), uint32(_random())) {} catch {} + try sam.setBasePrice(address(edition), uint32(_random())) {} catch {} + try sam.setAffiliateFee(address(edition), uint16(_random())) {} catch {} + try sam.setGoldenEggFee(address(edition), uint16(_random())) {} catch {} + try sam.setArtistFee(address(edition), uint16(_random())) {} catch {} + try sam.setPlatformFee(uint16(_random())) {} catch {} + + // If the contract is not watertight, one of the following asserts or transactions will fail. + + assertEq( + address(sam).balance, + samBaseBalance + + sam.totalValue(address(edition), 0, uint32(n)) + + sam.affiliateFeesAccrued(affiliate) + + sam.platformFeesAccrued() + + sam.goldenEggFeesAccrued(address(edition)) + ); + + uint256[] memory tokenIds = new uint256[](n); + if (_random() % 2 == 0) { + uint256 balance = sam.samInfo(address(edition)).balance; + if (balance != 0) { + sam.directSetPoolBalance(address(edition), balance - 1); + vm.expectRevert(bytes("WTF")); + sam.sell(address(edition), tokenIds, 0, exploiter); + sam.directSetPoolBalance(address(edition), balance); + } + } + sam.sell(address(edition), tokenIds, 0, exploiter); + + assertEq( + address(sam).balance, + samBaseBalance + + sam.affiliateFeesAccrued(affiliate) + + sam.platformFeesAccrued() + + sam.goldenEggFeesAccrued(address(edition)) + ); + + sam.withdrawForAffiliate(affiliate); + + assertEq( + address(sam).balance, + samBaseBalance + sam.platformFeesAccrued() + sam.goldenEggFeesAccrued(address(edition)) + ); + + sam.setPlatformFeeAddress(address(this)); + sam.withdrawForPlatform(); + + assertEq(address(sam).balance, samBaseBalance + sam.goldenEggFeesAccrued(address(edition))); + + sam.withdrawForGoldenEgg(address(edition)); + + assertEq(address(sam).balance, samBaseBalance); + + uint256 exploiterBalanceAfter = exploiter.balance; + require(exploiterBalanceAfter <= exploiterBalanceBefore, "Something is wrong"); + } + + function test_samBeforeAndAfterPrimarySales(uint256) public { + SoundEditionV1_2 edition = createGenericEdition(); + MockSAM sam = new MockSAM(); + + edition.setSAM(address(sam)); + + sam.create( + address(edition), + BASE_PRICE, + LINEAR_PRICE_SLOPE, + INFLECTION_PRICE, + INFLECTION_POINT, + MAX_SUPPLY, + BUY_FREEZE_TIME, + ARTIST_FEE_BPS, + GOLDEN_EGG_FEE_BPS, + AFFILIATE_FEE_BPS + ); + + uint256 editionMaxMintableLower = _random() % 32; + uint256 editionMaxMintableUpper = editionMaxMintableLower + (_random() % 32); + uint256 editionCutoffTime = block.timestamp + (_random() % 128); + edition.setEditionMaxMintableRange(uint32(editionMaxMintableLower), uint32(editionMaxMintableUpper)); + if (editionMaxMintableUpper != 0) { + edition.setEditionCutoffTime(uint32(editionCutoffTime)); + } + + while (true) { + uint256 quantity = _bound(_random(), 1, 4); + vm.warp(block.timestamp + (_random() % 16)); + uint256 editionMaxMintable = edition.editionMaxMintable(); + uint256 totalMinted = edition.totalMinted(); + if (totalMinted + quantity > editionMaxMintable) { + vm.expectRevert(); + edition.mint(address(this), quantity); + if (editionMaxMintable > totalMinted) { + edition.mint(address(this), editionMaxMintable - totalMinted); + } + break; + } else { + vm.expectRevert(ISoundEditionV1_2.MintNotConcluded.selector); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + 1, + address(0), + new bytes32[](0) + ); + if (edition.totalMinted() != 0) { + vm.expectRevert(ISoundEditionV1_2.MintsAlreadyExist.selector); + edition.setSAM(address(sam)); + } + edition.mint(address(this), quantity); + } + } + assertTrue(edition.mintConcluded()); + + vm.expectRevert(ISoundEditionV1_2.MintHasConcluded.selector); + edition.setSAM(address(sam)); + + if (block.timestamp >= editionCutoffTime) { + assertTrue(edition.totalMinted() >= editionMaxMintableLower); + } else { + assertEq(edition.totalMinted(), editionMaxMintableUpper); + } + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + } + + function test_samMint(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + t.fromTokenId = edition.nextTokenId(); + + uint256 n = _bound(_random(), 0, 100); + t.collectors = _randomCollectors(); + + if (n == 0) { + vm.expectRevert(IERC721AUpgradeable.MintZeroQuantity.selector); + } + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + uint256[] memory expectedTokenIds = new uint256[](n); + for (uint256 i; i < n; ++i) { + expectedTokenIds[i] = t.fromTokenId + i; + } + assertEq(expectedTokenIds, edition.tokensOfOwner(t.collectors[0])); + } + + function test_samBurnCannotBurnTokenZero(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + if (_random() % 2 == 0) { + tokenIdsToSell[0] = 0; + vm.expectRevert(IERC721AUpgradeable.OwnerQueryForNonexistentToken.selector); + } + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + } + + function test_samBurnUpdatesTokenExistences(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + + for (uint256 i; i != tokenIdsToSell.length; ++i) { + vm.expectRevert(IERC721AUpgradeable.ApprovalQueryForNonexistentToken.selector); + edition.getApproved(tokenIdsToSell[i]); + } + } + + function test_samBurnCannotBurnBurnedTokens(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + vm.prank(t.collectors[1]); + sam.buy{ value: address(t.collectors[1]).balance }( + address(edition), + t.collectors[1], + 10, + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + + for (uint256 j; j < 5; ++j) { + uint256[] memory partialTokenIdsToSell = new uint256[](tokenIdsToSell.length); + for (uint256 i; i != tokenIdsToSell.length; ++i) { + partialTokenIdsToSell[i] = tokenIdsToSell[_random() % tokenIdsToSell.length]; + } + LibSort.insertionSort(partialTokenIdsToSell); + LibSort.uniquifySorted(partialTokenIdsToSell); + vm.expectRevert(IERC721AUpgradeable.OwnerQueryForNonexistentToken.selector); + vm.prank(t.collectors[0]); + sam.sell(address(edition), partialTokenIdsToSell, 0, address(t.collectors[0])); + } + } + + function test_samBurnUpdatesBalanceAndNumberBurned(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + + // Mint more. + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(_bound(_random(), 1, 5)), + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + t.numBurnedBefore = edition.numberBurned(t.collectors[0]); + t.numCollectedBefore = edition.balanceOf(t.collectors[0]); + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + + assertEq(edition.numberBurned(t.collectors[0]) - t.numBurnedBefore, tokenIdsToSell.length); + assertEq(t.numCollectedBefore - edition.balanceOf(t.collectors[0]), tokenIdsToSell.length); + } + + function test_samBurnMustBeStrictlyAscending(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _mintOut(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + if (_random() % 8 != 0) { + bytes32 strictlyAscendingHash = keccak256(abi.encode(tokenIdsToSell)); + if (_random() % 8 != 0) { + LibPRNG.PRNG memory prng = LibPRNG.PRNG(_random()); + prng.shuffle(tokenIdsToSell); + } else { + tokenIdsToSell[_random() % tokenIdsToSell.length] = tokenIdsToSell[_random() % tokenIdsToSell.length]; + } + if (strictlyAscendingHash != keccak256(abi.encode(tokenIdsToSell))) { + vm.expectRevert(ISoundEditionV1_2.TokenIdsNotStrictlyAscending.selector); + } + } + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + } + + function test_samBurnMustBeApproved(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _mintOut(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + + if (_random() % 2 == 0) { + uint256 tokenIdTransferred = tokenIdsToSell[_random() % tokenIdsToSell.length]; + vm.prank(t.collectors[0]); + edition.transferFrom(t.collectors[0], t.collectors[1], tokenIdTransferred); + if (_random() % 2 == 0) { + vm.prank(t.collectors[1]); + edition.approve(t.collectors[0], tokenIdTransferred); + } else if (_random() % 2 == 0) { + vm.prank(t.collectors[1]); + edition.setApprovalForAll(t.collectors[0], true); + } else { + vm.expectRevert(IERC721AUpgradeable.TransferCallerNotOwnerNorApproved.selector); + } + } + vm.warp(block.timestamp + 60); + + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + } + + function test_samBurnSuccessAfterSetApprovalForAll(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _mintOut(edition); + + t.collectors = _randomCollectors(); + + for (uint256 i; i < 2; ++i) { + vm.prank(t.collectors[i]); + sam.buy{ value: address(t.collectors[i]).balance }( + address(edition), + t.collectors[i], + uint32(_bound(_random(), 1, 5)), + address(0), + new bytes32[](0) + ); + vm.prank(t.collectors[i]); + edition.setApprovalForAll(address(this), true); + + t.tokenIds[i] = edition.tokensOfOwner(t.collectors[i]); + } + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIdsToSell = new uint256[](sam.samInfo(address(edition)).supply); + uint256 o; + for (uint256 j; j < 2; ++j) { + for (uint256 i; i < t.tokenIds[j].length; ++i) { + tokenIdsToSell[o++] = t.tokenIds[j][i]; + } + } + + sam.sell(address(edition), tokenIdsToSell, 0, address(this)); + + for (uint256 i; i < 2; ++i) { + assertEq(edition.numberBurned(t.collectors[i]), t.tokenIds[i].length); + assertEq(edition.balanceOf(t.collectors[i]), 0); + } + } + + function test_samBurnSuccessForApproved(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _mintOut(edition); + + uint256 n = _bound(_random(), 1, 5); + t.collectors = _randomCollectors(); + + vm.prank(t.collectors[0]); + sam.buy{ value: address(t.collectors[0]).balance }( + address(edition), + t.collectors[0], + uint32(n), + address(0), + new bytes32[](0) + ); + + uint256[] memory tokenIdsToSell = edition.tokensOfOwner(t.collectors[0]); + + if (_random() % 2 == 0) { + uint256 tokenIdTransferred = tokenIdsToSell[_random() % tokenIdsToSell.length]; + vm.prank(t.collectors[0]); + edition.transferFrom(t.collectors[0], t.collectors[1], tokenIdTransferred); + if (_random() % 2 == 0) { + vm.prank(t.collectors[1]); + edition.approve(t.collectors[0], tokenIdTransferred); + } else if (_random() % 2 == 0) { + vm.prank(t.collectors[1]); + edition.setApprovalForAll(t.collectors[0], true); + } else { + vm.expectRevert(IERC721AUpgradeable.TransferCallerNotOwnerNorApproved.selector); + } + } + + vm.warp(block.timestamp + 60); + + vm.prank(t.collectors[0]); + sam.sell(address(edition), tokenIdsToSell, 0, address(t.collectors[0])); + } + + function test_samBuyNonExistingEditionReverts() public { + MockSAM sam = new MockSAM(); + + address nonExistingEdition = address(0xa11ce); + // This will be an EvmError revert because we + // are using plain Solidity to call a non-existing edition. + vm.expectRevert(); + + sam.buy{ value: address(this).balance }(nonExistingEdition, address(1), 1, address(0), new bytes32[](0)); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + // This will be an EvmError revert because we + // are using plain Solidity to call a non-existing edition + vm.expectRevert(); + sam.sell(nonExistingEdition, tokenIds, 0, address(this)); + } + + function test_samBuyOnEditionWhenMintNotConcludedReverts() public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + + vm.expectRevert(ISoundEditionV1_2.MintNotConcluded.selector); + sam.buy{ value: address(this).balance }(address(edition), address(1), 1, address(0), new bytes32[](0)); + + _maxMint(edition); + + sam.buy{ value: address(this).balance }(address(edition), address(1), 1, address(0), new bytes32[](0)); + } + + function test_samBuyOnEditionWithoutCreateReverts() public { + MockSAM sam = new MockSAM(); + + SoundEditionV1_2 edition = createGenericEdition(); + edition.setEditionMaxMintableRange(EDITION_MAX_MINTABLE_LOWER, EDITION_MAX_MINTABLE_LOWER); + edition.setEditionCutoffTime(0); + edition.setSAM(address(sam)); + + _maxMint(edition); + + vm.expectRevert(ISAM.SAMDoesNotExist.selector); + sam.buy{ value: address(this).balance }(address(edition), address(1), 1, address(0), new bytes32[](0)); + } + + function test_samSellOnEditionWithoutCreateReverts() public { + MockSAM sam = new MockSAM(); + + SoundEditionV1_2 edition = createGenericEdition(); + edition.setEditionMaxMintableRange(EDITION_MAX_MINTABLE_LOWER, EDITION_MAX_MINTABLE_LOWER); + edition.setEditionCutoffTime(0); + edition.setSAM(address(sam)); + + _maxMint(edition); + + uint256[] memory tokenIdsToSell = new uint256[](1); + tokenIdsToSell[0] = 1; + vm.expectRevert(ISAM.SAMDoesNotExist.selector); + sam.sell(address(edition), tokenIdsToSell, 0, address(this)); + } + + function test_samSellMoreThanSupplyReverts(uint256) public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _mintOut(edition); + + uint256 n = _bound(_random(), 1, 10); + uint256 fromTokenId = edition.nextTokenId(); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + uint32(n), + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + uint256 m = _bound(_random(), 1, 10); + uint256[] memory tokenIdsToSell = new uint256[](m); + for (uint256 i; i < m; ++i) { + tokenIdsToSell[i] = i + fromTokenId; + } + if (m > n) { + vm.expectRevert(abi.encodeWithSignature("InsufficientSupply(uint256,uint256)", n, m)); + } + sam.sell(address(edition), tokenIdsToSell, 0, address(this)); + } + + function test_samBuySellLargeBatch() public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + + uint256 n = 300; + uint256 fromTokenId = edition.nextTokenId(); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + uint32(n), + address(0), + new bytes32[](0) + ); + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIdsToSell = new uint256[](n); + for (uint256 i; i < n; ++i) { + tokenIdsToSell[i] = i + fromTokenId; + } + + sam.sell(address(edition), tokenIdsToSell, 0, address(this)); + for (uint256 i; i != tokenIdsToSell.length; ++i) { + vm.expectRevert(IERC721AUpgradeable.ApprovalQueryForNonexistentToken.selector); + edition.getApproved(tokenIdsToSell[i]); + } + } + + function test_samDoesNotChangeGoldenEggId(uint256) public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _mintOut(edition); + bool hasGoldenEgg = edition.totalMinted() != 0; + uint256 goldenEggIdBefore = hasGoldenEgg ? _getGoldenEggId(edition) : 0; + + uint256 n = _bound(_random(), 1, 10); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + uint32(n), + address(0), + new bytes32[](0) + ); + uint256 goldenEggIdAfter = hasGoldenEgg ? _getGoldenEggId(edition) : 0; + assertEq(goldenEggIdAfter, goldenEggIdBefore); + } + + function test_samBuySellRepeatedly(uint256) public { + /* ------------------------- SETUP -------------------------- */ + + _testTempVariables memory t; + + SoundEditionV1_2[] memory editions = new SoundEditionV1_2[](2); + t.goldenEggIds = new uint256[](editions.length); + + MockSAM sam = new MockSAM(); + + sam.setPlatformFee(PLATFORM_FEE_BPS); + + for (uint256 i; i < editions.length; ++i) { + SoundEditionV1_2 edition = createGenericEdition(); + edition.setEditionMaxMintableRange(EDITION_MAX_MINTABLE_LOWER, EDITION_MAX_MINTABLE_LOWER); + edition.setEditionCutoffTime(0); + edition.setSAM(address(sam)); + _setOpenGoldenEggMetadataModule(edition); + + if (_random() % 4 == 0) { + t.basePrice = _bound(_random(), 0, type(uint96).max); + t.linearPriceSlope = _bound(_random(), 1, type(uint96).max); + t.inflectionPrice = _bound(_random(), 1, type(uint96).max); + t.inflectionPoint = _bound(_random(), 1, type(uint32).max); + } else { + t.basePrice = BASE_PRICE; + t.linearPriceSlope = LINEAR_PRICE_SLOPE; + t.inflectionPrice = INFLECTION_PRICE; + t.inflectionPoint = INFLECTION_POINT; + } + + t.artistFeeBPS = _bound(_random(), 0, sam.MAX_ARTIST_FEE_BPS()); + t.goldenEggFeeBPS = _bound(_random(), 0, sam.MAX_GOLDEN_EGG_FEE_BPS()); + t.affiliateFeeBPS = _bound(_random(), 0, sam.MAX_AFFILIATE_FEE_BPS()); + + sam.create( + address(edition), + uint96(t.basePrice), + uint128(t.linearPriceSlope), + uint128(t.inflectionPrice), + uint32(t.inflectionPoint), + MAX_SUPPLY, + BUY_FREEZE_TIME, + uint16(t.artistFeeBPS), + uint16(t.goldenEggFeeBPS), + uint16(t.affiliateFeeBPS) + ); + + _maxMint(edition); + + t.goldenEggIds[i] = _getGoldenEggId(edition); + editions[i] = edition; + } + + t.collectors = _randomCollectors(); + t.affiliates = _randomCollectors(); + + /* ---------------------- TEST BUY SELL --------------------- */ + + for (uint256 i = 2 + (_random() % 4); i != 0; --i) { + address collector = t.collectors[_random() % t.collectors.length]; + address affiliate = t.affiliates[_random() % t.affiliates.length]; + SoundEditionV1_2 edition = editions[_random() % editions.length]; + (uint256 totalInflows, uint256 totalOutflows) = _testBuySell(edition, sam, collector, affiliate); + t.totalInflows += totalInflows; + t.totalInflows -= totalOutflows; + } + + /* ---------- CHECK WITHDRAWING FEES AND BALANCES ----------- */ + + for (uint256 i; i < editions.length; ++i) { + SoundEditionV1_2 edition = editions[i]; + SAMInfo memory samInfo = sam.samInfo(address(edition)); + uint256 balance = sam.totalSellPrice(address(edition), 0, samInfo.supply); + assertEq(samInfo.balance, balance); + t.totalPoolValue += balance; + t.totalGoldenEggFeesAccrued += sam.goldenEggFeesAccrued(address(edition)); + t.totalArtistFeesAccrued += address(edition).balance; + } + + for (uint256 i; i < t.affiliates.length; ++i) { + t.totalAffiliateFeesAccrued += sam.affiliateFeesAccrued(t.affiliates[i]); + } + + t.platformFeesAccrued = sam.platformFeesAccrued(); + + assertEq( + t.totalPoolValue + t.totalGoldenEggFeesAccrued + t.totalAffiliateFeesAccrued + t.platformFeesAccrued, + address(sam).balance + ); + assertEq(address(sam).balance + t.totalArtistFeesAccrued, t.totalInflows); + + _testWithdrawForPlatform(sam); + assertEq(t.totalPoolValue + t.totalGoldenEggFeesAccrued + t.totalAffiliateFeesAccrued, address(sam).balance); + + for (uint256 i; i < t.affiliates.length; ++i) { + _testWithdrawForAffiliate(sam, t.affiliates[i]); + } + assertEq(t.totalPoolValue + t.totalGoldenEggFeesAccrued, address(sam).balance); + for (uint256 i; i < editions.length; ++i) { + _testWithdrawForGoldenEgg(sam, editions[i]); + assertEq(_getGoldenEggId(editions[i]), t.goldenEggIds[i]); + } + assertEq(t.totalPoolValue, address(sam).balance); + + if (t.totalGoldenEggFeesAccrued != 0) { + t.totalGoldenEggFeesAccrued = 0; + vm.deal(address(this), type(uint192).max); + for (uint256 i; i < editions.length; ++i) { + sam.buy{ value: address(this).balance }( + address(editions[i]), + address(this), + 1, + address(1), + new bytes32[](0) + ); + t.totalGoldenEggFeesAccrued += sam.goldenEggFeesAccrued(address(editions[i])); + _testWithdrawForGoldenEgg(sam, editions[i]); + } + assertTrue(t.totalGoldenEggFeesAccrued > 0); + } + + /* -------- TEST SELLING BELOW MINIMUM SUPPLY REVERTS ------- */ + + vm.warp(block.timestamp + 60); + + for (uint256 i; i < editions.length; ++i) { + SoundEditionV1_2 edition = editions[i]; + _testSellRemainingTokens(edition, sam); + } + + /* --------------------- TEST FREEZE BUY -------------------- */ + + vm.warp(block.timestamp + 60); + + if (_random() % 8 == 0) { + for (uint256 i; i < editions.length; ++i) { + SoundEditionV1_2 edition = editions[i]; + _testFreezeBuy(edition, sam); + } + } + } + + function _testSellRemainingTokens(SoundEditionV1_2 edition, MockSAM sam) internal { + vm.warp(block.timestamp + 60); + + _testTempVariables memory t; + uint256[] memory tokenIds = edition.tokensOfOwner(address(this)); + + uint256 remainingSupply = sam.samInfo(address(edition)).supply; + + if (tokenIds.length != 0) { + t.tokenIds[0] = new uint256[](_bound(_random(), 1, tokenIds.length)); + for (uint256 i; i < t.tokenIds[0].length; ++i) { + t.tokenIds[0][i] = tokenIds[i]; + assertEq(edition.ownerOf(tokenIds[i]), address(this)); + } + bool hasRevert; + if (remainingSupply < t.tokenIds[0].length) { + vm.expectRevert( + abi.encodeWithSignature( + "InsufficientSupply(uint256,uint256)", + remainingSupply, + t.tokenIds[0].length + ) + ); + hasRevert = true; + } + sam.sell(address(edition), t.tokenIds[0], 0, address(this)); + + if (_random() % 2 == 0) { + remainingSupply = sam.samInfo(address(edition)).supply; + tokenIds = edition.tokensOfOwner(address(this)); + + if (tokenIds.length >= remainingSupply) { + t.tokenIds[0] = new uint256[](remainingSupply); + for (uint256 i; i < remainingSupply; ++i) { + t.tokenIds[0][i] = tokenIds[i]; + } + if (t.tokenIds[0].length != 0) { + sam.sell(address(edition), t.tokenIds[0], 0, address(this)); + } + SAMInfo memory samInfo = sam.samInfo(address(edition)); + assertEq(samInfo.supply, 0); + assertEq(samInfo.balance, 0); + } + } + } + } + + function _testWithdrawForPlatform(MockSAM sam) internal { + (address feeAddr, ) = _randomSigner(); + + vm.expectRevert(ISAM.PlatformFeeAddressIsZero.selector); + sam.setPlatformFeeAddress(address(0)); + + vm.expectRevert(Ownable.Unauthorized.selector); + vm.prank(address(1)); + sam.setPlatformFeeAddress(feeAddr); + + vm.expectEmit(true, true, true, true); + emit PlatformFeeAddressSet(feeAddr); + sam.setPlatformFeeAddress(feeAddr); + + uint256 accrued = sam.platformFeesAccrued(); + uint256 balanceBefore = address(feeAddr).balance; + if (accrued != 0) { + vm.expectEmit(true, true, true, true); + emit PlatformFeesWithdrawn(SafeCastLib.toUint128(accrued)); + } + sam.withdrawForPlatform(); + assertEq(address(feeAddr).balance - balanceBefore, accrued); + assertEq(sam.platformFeesAccrued(), 0); + } + + function _testWithdrawForAffiliate(MockSAM sam, address affiliate) internal { + uint256 accrued = sam.affiliateFeesAccrued(affiliate); + uint256 balanceBefore = address(affiliate).balance; + if (accrued != 0) { + vm.expectEmit(true, true, true, true); + emit AffiliateFeesWithdrawn(affiliate, SafeCastLib.toUint128(accrued)); + } + sam.withdrawForAffiliate(affiliate); + assertEq(address(affiliate).balance - balanceBefore, accrued); + assertEq(sam.affiliateFeesAccrued(affiliate), 0); + } + + function _testWithdrawForGoldenEgg(MockSAM sam, SoundEditionV1_2 edition) internal { + uint256 accrued = sam.goldenEggFeesAccrued(address(edition)); + uint256 goldenEggId = IOpenGoldenEggMetadata(edition.metadataModule()).getGoldenEggTokenId(address(edition)); + address goldenEggOwner = edition.ownerOf(goldenEggId); + uint256 balanceBefore = address(goldenEggOwner).balance; + if (accrued != 0) { + vm.expectEmit(true, true, true, true); + emit GoldenEggFeesWithdrawn(address(edition), goldenEggOwner, SafeCastLib.toUint128(accrued)); + } + sam.withdrawForGoldenEgg(address(edition)); + assertEq(address(goldenEggOwner).balance - balanceBefore, accrued); + assertEq(sam.goldenEggFeesAccrued(address(edition)), 0); + } + + function _totalBuyPrice( + SoundEditionV1_2 edition, + MockSAM sam, + uint256 fromSupply, + uint256 quantity + ) internal view returns (uint256 result) { + (result, , , , ) = sam.totalBuyPriceAndFees(address(edition), uint32(fromSupply), uint32(quantity)); + } + + function _testBuySell( + SoundEditionV1_2 edition, + MockSAM sam, + address collector, + address affiliate + ) internal returns (uint256 totalInflows, uint256 totalOutflows) { + /* ------------------------- SETUP -------------------------- */ + + _testTempVariables memory t; + + t.quantities[0] = _bound(_random(), 1, 3); + t.quantities[1] = _bound(_random(), 1, 3); + + t.totalBuyPrices[0] = _totalBuyPrice(edition, sam, 0, t.quantities[0]); + t.totalBuyPrices[1] = _totalBuyPrice(edition, sam, t.quantities[0], t.quantities[1]); + + totalInflows += t.totalBuyPrices[0] + t.totalBuyPrices[1]; + + /* ------------------------ TEST BUY ------------------------ */ + + vm.startPrank(collector); + t.numCollectedBefore = edition.balanceOf(collector); + t.numMintedBefore = edition.numberMinted(collector); + t.totalSupplyBefore = edition.totalSupply(); + + // Check events. + { + SAMInfo memory samInfo = sam.samInfo(address(edition)); + t.totalFees = + t.totalBuyPrices[0] - + sam.totalValue(address(edition), samInfo.supply, uint32(t.quantities[0])); + t.platformFeeBPS = sam.platformFeeBPS(); + t.fromTokenId = edition.nextTokenId(); + t.totalFeeBPS = t.platformFeeBPS + samInfo.artistFeeBPS + samInfo.affiliateFeeBPS + samInfo.goldenEggFeeBPS; + t.feePerBPS = t.totalFees / t.totalFeeBPS; + t.platformFee = t.feePerBPS * uint256(t.platformFeeBPS); + t.artistFee = t.feePerBPS * uint256(samInfo.artistFeeBPS); + t.goldenEggFee = t.feePerBPS * uint256(samInfo.goldenEggFeeBPS); + t.affiliateFee = t.feePerBPS * uint256(samInfo.affiliateFeeBPS); + t.attributionId = _random(); + t.affiliate = affiliate; + + vm.expectEmit(true, true, true, true); + emit Bought( + address(edition), + collector, + t.fromTokenId, + samInfo.supply, + uint32(t.quantities[0]), + uint128(t.totalBuyPrices[0]), + uint128(t.platformFee), + uint128(t.artistFee), + uint128(t.goldenEggFee), + uint128(t.affiliateFee), + t.affiliate, + true, + t.attributionId + ); + } + + sam.buy{ value: t.totalBuyPrices[0] }( + address(edition), + collector, + uint32(t.quantities[0]), + affiliate, + new bytes32[](0), + t.attributionId + ); + + // Check underpaying reverts. + if (_random() % 2 == 0 && t.totalBuyPrices[1] != 0) { + vm.expectRevert( + abi.encodeWithSignature("Underpaid(uint256,uint256)", t.totalBuyPrices[1] - 1, t.totalBuyPrices[1]) + ); + sam.buy{ value: t.totalBuyPrices[1] - 1 }( + address(edition), + collector, + uint32(t.quantities[1]), + affiliate, + new bytes32[](0) + ); + } + t.balancesBefore[0] = address(collector).balance; + sam.buy{ value: t.totalBuyPrices[1] + _bound(_random(), 0, 1 ether) }( + address(edition), + collector, + uint32(t.quantities[1]), + affiliate, + new bytes32[](0) + ); + + { + uint256 numBought = t.quantities[0] + t.quantities[1]; + assertEq(edition.balanceOf(collector) - t.numCollectedBefore, numBought); + assertEq(edition.totalSupply() - t.totalSupplyBefore, numBought); + assertEq(edition.numberMinted(collector) - t.numMintedBefore, numBought); + assertEq(t.balancesBefore[0] - address(collector).balance, t.totalBuyPrices[1]); + } + + /* ------------------------ TEST SELL ----------------------- */ + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIds = edition.tokensOfOwner(collector); + + t.tokenIds[0] = new uint256[](_bound(_random(), 1, tokenIds.length)); + for (uint256 i; i < t.tokenIds[0].length; ++i) { + t.tokenIds[0][i] = tokenIds[i]; + } + + t.tokenIds[1] = new uint256[](0); + if (tokenIds.length > t.tokenIds[0].length) { + t.tokenIds[1] = new uint256[](_bound(_random(), 1, tokenIds.length - t.tokenIds[0].length)); + } + for (uint256 i; i < t.tokenIds[1].length; ++i) { + t.tokenIds[1][i] = tokenIds[t.tokenIds[0].length + i]; + } + + t.numCollectedBefore = edition.balanceOf(collector); + t.numBurnedBefore = edition.numberBurned(collector); + t.numMintedBefore = edition.numberMinted(collector); + t.totalSupplyBefore = edition.totalSupply(); + + t.totalSellPrices[0] = sam.totalSellPrice(address(edition), 0, uint32(t.tokenIds[0].length)); + t.totalSellPrices[1] = sam.totalSellPrice( + address(edition), + uint32(t.tokenIds[0].length), + uint32(t.tokenIds[1].length) + ); + + totalOutflows += t.totalSellPrices[0] + t.totalSellPrices[1]; + t.attributionId = _random(); + { + SAMInfo memory samInfo = sam.samInfo(address(edition)); + for (uint256 i; i < t.tokenIds[0].length; ++i) { + vm.expectEmit(true, true, true, true); + emit Transfer(collector, address(0), t.tokenIds[0][i]); + } + if (t.tokenIds[0].length != 0) { + vm.expectEmit(true, true, true, true); + emit Sold( + address(edition), + collector, + samInfo.supply, + t.tokenIds[0], + uint128(t.totalSellPrices[0]), + t.attributionId + ); + } + } + if (_random() % 8 == 0) _checkAllExists(edition, collector, t.tokenIds[0]); + if (t.tokenIds[0].length == 0) { + vm.expectRevert(ISAM.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); + } else { + vm.expectRevert( + abi.encodeWithSignature( + "InsufficientPayout(uint256,uint256)", + t.totalSellPrices[1], + t.totalSellPrices[1] + 1 + ) + ); + } + sam.sell(address(edition), t.tokenIds[1], t.totalSellPrices[1] + 1, collector); + } + + // Check events. + { + SAMInfo memory samInfo = sam.samInfo(address(edition)); + for (uint256 i; i < t.tokenIds[1].length; ++i) { + vm.expectEmit(true, true, true, true); + emit Transfer(collector, address(0), t.tokenIds[1][i]); + } + if (t.tokenIds[1].length != 0) { + vm.expectEmit(true, true, true, true); + emit Sold(address(edition), collector, samInfo.supply, t.tokenIds[1], uint128(t.totalSellPrices[1]), 0); + } + } + 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); + } + sam.sell(address(edition), t.tokenIds[1], t.totalSellPrices[1], collector); + if (_random() % 8 == 0) _checkAllBurned(edition, collector, t.tokenIds[1]); + + { + uint256 numSold = t.tokenIds[0].length + t.tokenIds[1].length; + assertEq(t.numCollectedBefore - edition.balanceOf(collector), numSold); + assertEq(t.totalSupplyBefore - edition.totalSupply(), numSold); + assertEq(edition.numberBurned(collector) - t.numBurnedBefore, numSold); + assertEq(edition.numberMinted(collector), t.numMintedBefore); + } + + vm.stopPrank(); + } + + function _checkAllExists( + SoundEditionV1_2 edition, + address collector, + uint256[] memory tokenIds + ) internal { + unchecked { + for (uint256 i; i < tokenIds.length; ++i) { + IERC721AUpgradeable.TokenOwnership memory ownership = edition.explicitOwnershipOf(tokenIds[i]); + assertEq(ownership.burned, false); + assertEq(ownership.addr, collector); + } + } + } + + function _checkAllBurned( + SoundEditionV1_2 edition, + address collector, + uint256[] memory tokenIds + ) internal { + unchecked { + for (uint256 i; i < tokenIds.length; ++i) { + IERC721AUpgradeable.TokenOwnership memory ownership = edition.explicitOwnershipOf(tokenIds[i]); + assertEq(ownership.burned, true); + assertEq(ownership.startTimestamp, block.timestamp); + assertEq(ownership.addr, collector); + } + } + } + + function _testFreezeBuy(SoundEditionV1_2 edition, MockSAM sam) internal { + vm.deal(address(this), type(uint192).max); + uint256 startTokenId = edition.nextTokenId(); + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + + vm.expectEmit(true, true, true, true); + emit BuyFreezeTimeSet(address(edition), uint32(block.timestamp)); + sam.setBuyFreezeTime(address(edition), uint32(block.timestamp)); + + vm.expectRevert(ISAM.BuyIsFrozen.selector); + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + + vm.warp(block.timestamp + 60); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = startTokenId; + sam.sell(address(edition), tokenIds, 0, address(this)); + } + + function test_samSetBuyFreezeTime() public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + uint256 buyFreezeTime = block.timestamp + 10; + + sam.setBuyFreezeTime(address(edition), uint32(0)); + sam.setBuyFreezeTime(address(edition), uint32(1)); + sam.setBuyFreezeTime(address(edition), uint32(2)); + + vm.expectEmit(true, true, true, true); + emit BuyFreezeTimeSet(address(edition), uint32(buyFreezeTime + 10)); + sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime + 10)); + + _mintOut(edition); + + vm.expectRevert(ISAM.InvalidBuyFreezeTime.selector); + sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime + 11)); + + sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime + 10)); + sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime + 9)); + + sam.setBuyFreezeTime(address(edition), uint32(buyFreezeTime)); + + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + + vm.warp(buyFreezeTime - 1); + + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + + vm.warp(buyFreezeTime); + + vm.expectRevert(ISAM.BuyIsFrozen.selector); + sam.buy{ value: address(this).balance }(address(edition), address(this), 1, address(0), new bytes32[](0)); + } + + function test_samSetMaxSupply() public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + + uint32 maxSupply = 100; + sam.setMaxSupply(address(edition), uint32(0)); + sam.setMaxSupply(address(edition), uint32(1)); + sam.setMaxSupply(address(edition), uint32(2)); + + vm.expectEmit(true, true, true, true); + emit MaxSupplySet(address(edition), uint32(maxSupply + 10)); + sam.setMaxSupply(address(edition), uint32(maxSupply + 10)); + + _mintOut(edition); + + vm.expectRevert(ISAM.InvalidMaxSupply.selector); + sam.setMaxSupply(address(edition), uint32(maxSupply + 11)); + + sam.setMaxSupply(address(edition), uint32(maxSupply)); + + sam.buy{ value: address(this).balance }(address(edition), address(this), 3, address(0), new bytes32[](0)); + + vm.expectRevert(abi.encodeWithSignature("ExceedsMaxSupply(uint32)", uint32(maxSupply - 3))); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + maxSupply, + address(0), + new bytes32[](0) + ); + } + + function test_samCreate(uint256) public { + _testTempVariables memory t; + + SoundEditionV1_2 edition = createGenericEdition(); + + MockSAM sam = new MockSAM(); + + edition.setEditionMaxMintableRange(EDITION_MAX_MINTABLE_LOWER, EDITION_MAX_MINTABLE_LOWER); + edition.setSAM(address(sam)); + + t.maxArtistFeeBPS = sam.MAX_ARTIST_FEE_BPS(); + t.maxAffiliateFeeBPS = sam.MAX_AFFILIATE_FEE_BPS(); + t.maxGoldenEggFeeBPS = sam.MAX_GOLDEN_EGG_FEE_BPS(); + t.maxPlatformFeeBPS = sam.MAX_PLATFORM_FEE_BPS(); + + t.basePrice = _bound(_random(), 0, type(uint96).max); + t.inflectionPrice = _bound(_random(), 0, type(uint128).max); + t.inflectionPoint = _bound(_random(), 0, type(uint32).max); + t.maxSupply = _bound(_random(), 0, type(uint32).max); + t.buyFreezeTime = _bound(_random(), 0, type(uint32).max); + t.artistFeeBPS = _bound(_random(), 0, t.maxArtistFeeBPS + 1000); + t.affiliateFeeBPS = _bound(_random(), 0, t.maxAffiliateFeeBPS + 1000); + t.goldenEggFeeBPS = _bound(_random(), 0, t.maxGoldenEggFeeBPS + 1000); + + bool hasRevert; + + if (_random() % 64 == 0) { + edition.transferOwnership(address(1)); + vm.expectRevert(Ownable.Unauthorized.selector); + hasRevert = true; + } else if (t.maxSupply == 0) { + vm.expectRevert(ISAM.InvalidMaxSupply.selector); + hasRevert = true; + } else if (t.buyFreezeTime == 0) { + vm.expectRevert(ISAM.InvalidBuyFreezeTime.selector); + hasRevert = true; + } else if (t.artistFeeBPS > t.maxArtistFeeBPS) { + vm.expectRevert(ISAM.InvalidArtistFeeBPS.selector); + hasRevert = true; + } else if (t.goldenEggFeeBPS > t.maxGoldenEggFeeBPS) { + vm.expectRevert(ISAM.InvalidGoldenEggFeeBPS.selector); + hasRevert = true; + } else if (t.affiliateFeeBPS > t.maxAffiliateFeeBPS) { + vm.expectRevert(ISAM.InvalidAffiliateFeeBPS.selector); + hasRevert = true; + } else if (_random() % 2 == 0) { + _maxMint(edition); + vm.expectRevert(ISAM.InSAMPhase.selector); + hasRevert = true; + } + + if (!hasRevert) { + // Test if an admin of the edition is authorized to create. + if (_random() % 2 == 0) { + edition.grantRoles(address(this), edition.ADMIN_ROLE()); + edition.transferOwnership(address(1)); + } + vm.expectEmit(true, true, true, true); + emit Created( + address(edition), + uint96(t.basePrice), + uint128(t.linearPriceSlope), + uint128(t.inflectionPrice), + uint32(t.inflectionPoint), + uint32(t.maxSupply), + uint32(t.buyFreezeTime), + uint16(t.artistFeeBPS), + uint16(t.goldenEggFeeBPS), + uint16(t.affiliateFeeBPS) + ); + } + + sam.create( + address(edition), + uint96(t.basePrice), + uint128(t.linearPriceSlope), + uint128(t.inflectionPrice), + uint32(t.inflectionPoint), + uint32(t.maxSupply), + uint32(t.buyFreezeTime), + uint16(t.artistFeeBPS), + uint16(t.goldenEggFeeBPS), + uint16(t.affiliateFeeBPS) + ); + + if (hasRevert) return; + + // Test if repeated creation for the same edition is not allowed. + if (_random() % 2 == 0) { + vm.expectRevert(ISAM.SAMAlreadyExists.selector); + sam.create( + address(edition), + uint96(t.basePrice), + uint128(t.linearPriceSlope), + uint128(t.inflectionPrice), + uint32(t.inflectionPoint), + uint32(t.maxSupply), + uint32(t.buyFreezeTime), + uint16(t.artistFeeBPS), + uint16(t.goldenEggFeeBPS), + uint16(t.affiliateFeeBPS) + ); + } + + // Check if `create` properly initializes the variables. + SAMInfo memory info = sam.samInfo(address(edition)); + assertEq(info.basePrice, t.basePrice); + assertEq(info.linearPriceSlope, t.linearPriceSlope); + assertEq(info.inflectionPrice, t.inflectionPrice); + assertEq(info.inflectionPoint, t.inflectionPoint); + assertEq(info.maxSupply, t.maxSupply); + assertEq(info.buyFreezeTime, t.buyFreezeTime); + assertEq(info.artistFeeBPS, t.artistFeeBPS); + assertEq(info.goldenEggFeeBPS, t.goldenEggFeeBPS); + assertEq(info.affiliateFeeBPS, t.affiliateFeeBPS); + } + + function test_goldenEggFeeRecipient() public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + // If the golden egg has not been revealed, the recipient is the `edition`. + assertEq(sam.goldenEggFeeRecipient(address(edition)), address(edition)); + + // Check if the function reverts if the edition is not a valid contract. + vm.expectRevert(); // Reverts with EvmError. + sam.goldenEggFeeRecipient(address(0xa11ce)); + + _maxMint(edition); + // If there is an golden egg, the recipient is the golden egg owner. + assertEq(sam.goldenEggFeeRecipient(address(edition)), edition.ownerOf(_getGoldenEggId(edition))); + // Burn the golden egg and check that the recipient is the edition. + edition.burn(_getGoldenEggId(edition)); + assertEq(sam.goldenEggFeeRecipient(address(edition)), address(edition)); + + // Recreate `edition` and `sam`. + (edition, sam) = _createEditionAndSAM(); + + edition.setMetadataModule(address(0)); + _maxMint(edition); + // If there is no golden egg metadata module, the recipient is the edition itself. + assertEq(sam.goldenEggFeeRecipient(address(edition)), address(edition)); + + // Recreate `edition` and `sam`. + (edition, sam) = _createEditionAndSAM(); + + edition.setMetadataModule(address(0)); + edition.setEditionMaxMintableRange(0, 0); + // If there is no golden egg, the recipient is the edition itself. + assertEq(sam.goldenEggFeeRecipient(address(edition)), address(edition)); + } + + function test_withdrawGoldenEggFees() public { + vm.deal(address(this), type(uint192).max); + + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + _maxMint(edition); + + assertEq(sam.goldenEggFeesAccrued(address(edition)), 0); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + uint32(_bound(_random(), 1, 10)), + address(0), + new bytes32[](0) + ); + uint256 accrued = sam.goldenEggFeesAccrued(address(edition)); + assertTrue(accrued != 0); + + uint256 balanceBefore = address(this).balance; + sam.withdrawForGoldenEgg(address(edition)); + assertEq(address(this).balance - balanceBefore, accrued); + + // Try again, but without a golden egg winner due to zero `editionMaxMintable`. + + (edition, sam) = _createEditionAndSAM(); + edition.setEditionMaxMintableRange(0, 0); + + assertEq(sam.goldenEggFeesAccrued(address(edition)), 0); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + uint32(_bound(_random(), 1, 10)), + address(0), + new bytes32[](0) + ); + accrued = sam.goldenEggFeesAccrued(address(edition)); + assertTrue(accrued != 0); + balanceBefore = address(edition).balance; + sam.withdrawForGoldenEgg(address(edition)); + assertEq(address(edition).balance - balanceBefore, accrued); + + // Try again, but without a golden egg winner due to no `metadataModule`. + + (edition, sam) = _createEditionAndSAM(); + edition.setMetadataModule(address(0)); + _maxMint(edition); + + assertEq(sam.goldenEggFeesAccrued(address(edition)), 0); + sam.buy{ value: address(this).balance }( + address(edition), + address(this), + uint32(_bound(_random(), 1, 10)), + address(0), + new bytes32[](0) + ); + accrued = sam.goldenEggFeesAccrued(address(edition)); + assertTrue(accrued != 0); + balanceBefore = address(edition).balance; + sam.withdrawForGoldenEgg(address(edition)); + assertEq(address(edition).balance - balanceBefore, accrued); + } + + function test_generalSettersAndGetters(uint256) public { + _testTempVariables memory t; + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + + t.maxArtistFeeBPS = sam.MAX_ARTIST_FEE_BPS(); + t.maxAffiliateFeeBPS = sam.MAX_AFFILIATE_FEE_BPS(); + t.maxGoldenEggFeeBPS = sam.MAX_GOLDEN_EGG_FEE_BPS(); + t.maxPlatformFeeBPS = sam.MAX_PLATFORM_FEE_BPS(); + + // Fuzz test for all the functions guarded by the + // `onlyEditionOwnerOrAdmin` and `onlyBeforeSAMPhase` modifiers. + for (uint256 i; i < 32; ++i) { + bool hasRevert; + bool prankUnauthorized; + bool mintHasConcluded; + + uint256 r = _random() % 7; + + // Test whether an unauthorized address will be reverted + // if the function is guarded by the `onlyEditionOwnerOrAdmin` modifier. + if (_random() % 16 == 0) { + vm.startPrank(address(1)); + vm.expectRevert(Ownable.Unauthorized.selector); + prankUnauthorized = true; + hasRevert = true; + } + + // Test whether setting a parameter that is guarded by + // the `onlyBeforeSAMPhase` modifier reverts. + if (_random() % 16 == 0 && !hasRevert && !(r == 4 || r == 6)) { + _mintOut(edition); + vm.expectRevert(ISAM.InSAMPhase.selector); + mintHasConcluded = true; + hasRevert = true; + } + + // Test set and get the `basePrice`. + if (r == 0) { + t.basePrice = _bound(_random(), 0, type(uint96).max); + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit BasePriceSet(address(edition), uint96(t.basePrice)); + } + sam.setBasePrice(address(edition), uint96(t.basePrice)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).basePrice, t.basePrice); + continue; + } + } + // Test set and get the `linearPriceSlope`. + if (r == 1) { + t.linearPriceSlope = _bound(_random(), 0, type(uint128).max); + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit LinearPriceSlopeSet(address(edition), uint128(t.linearPriceSlope)); + } + sam.setLinearPriceSlope(address(edition), uint128(t.linearPriceSlope)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).linearPriceSlope, t.linearPriceSlope); + continue; + } + } + // Test set and get the `inflectionPrice`. + if (r == 2) { + t.inflectionPrice = _bound(_random(), 0, type(uint128).max); + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit InflectionPriceSet(address(edition), uint128(t.inflectionPrice)); + } + sam.setInflectionPrice(address(edition), uint128(t.inflectionPrice)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).inflectionPrice, t.inflectionPrice); + continue; + } + } + // Test set and get the `inflectionPoint`. + if (r == 3) { + t.inflectionPoint = _bound(_random(), 0, type(uint32).max); + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit InflectionPointSet(address(edition), uint32(t.inflectionPoint)); + } + sam.setInflectionPoint(address(edition), uint32(t.inflectionPoint)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).inflectionPoint, t.inflectionPoint); + continue; + } + } + // Test set and get the `inflectionPoint`. + if (r == 4) { + t.artistFeeBPS = _bound(_random(), 0, t.maxArtistFeeBPS * 2); + if (t.artistFeeBPS > t.maxArtistFeeBPS && !hasRevert) { + vm.expectRevert(ISAM.InvalidArtistFeeBPS.selector); + hasRevert = true; + } + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit ArtistFeeSet(address(edition), uint16(t.artistFeeBPS)); + } + sam.setArtistFee(address(edition), uint16(t.artistFeeBPS)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).artistFeeBPS, t.artistFeeBPS); + continue; + } + } + // Test set and get the `goldenEggFeeBPS`. + if (r == 5) { + t.goldenEggFeeBPS = _bound(_random(), 0, t.maxGoldenEggFeeBPS * 2); + if (t.goldenEggFeeBPS > t.maxGoldenEggFeeBPS && !hasRevert) { + vm.expectRevert(ISAM.InvalidGoldenEggFeeBPS.selector); + hasRevert = true; + } + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit GoldenEggFeeSet(address(edition), uint16(t.goldenEggFeeBPS)); + } + sam.setGoldenEggFee(address(edition), uint16(t.goldenEggFeeBPS)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).goldenEggFeeBPS, t.goldenEggFeeBPS); + continue; + } + } + // Test set and get the `affiliateFeeBPS`. + if (r == 6) { + t.affiliateFeeBPS = _bound(_random(), 0, t.maxAffiliateFeeBPS * 2); + if (t.affiliateFeeBPS > t.maxAffiliateFeeBPS && !hasRevert) { + vm.expectRevert(ISAM.InvalidAffiliateFeeBPS.selector); + hasRevert = true; + } + if (!hasRevert) { + vm.expectEmit(true, true, true, true); + emit AffiliateFeeSet(address(edition), uint16(t.affiliateFeeBPS)); + } + sam.setAffiliateFee(address(edition), uint16(t.affiliateFeeBPS)); + if (!hasRevert) { + assertEq(sam.samInfo(address(edition)).affiliateFeeBPS, t.affiliateFeeBPS); + continue; + } + } + + if (prankUnauthorized) { + vm.stopPrank(); + } + + if (mintHasConcluded) { + (edition, sam) = _createEditionAndSAM(); + } + } + } + + function test_setAffiliateMerkleRoot() public { + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + + address[] memory accounts = new address[](2); + accounts[0] = address(0xa11ce); + accounts[1] = address(0xb0b); + + bytes32[] memory leaves = new bytes32[](accounts.length); + for (uint256 i = 0; i < accounts.length; ++i) { + leaves[i] = keccak256(abi.encodePacked(accounts[i])); + } + + // Test set and get the `affiliateMerkleRoot`. + Merkle m = new Merkle(); + bytes32 root = m.getRoot(leaves); + vm.expectEmit(true, true, true, true); + emit AffiliateMerkleRootSet(address(edition), root); + sam.setAffiliateMerkleRoot(address(edition), root); + assertEq(sam.affiliateMerkleRoot(address(edition)), root); + + // Test the `isAffiliatedWithProof` function. + assertTrue(sam.isAffiliatedWithProof(address(edition), accounts[0], m.getProof(leaves, 0))); + assertTrue(sam.isAffiliatedWithProof(address(edition), accounts[1], m.getProof(leaves, 1))); + assertFalse(sam.isAffiliatedWithProof(address(edition), address(0xbad), m.getProof(leaves, 1))); + + // Test the `onlyEditionOwnerOrAdmin` modifier. + vm.prank(address(1)); + vm.expectRevert(Ownable.Unauthorized.selector); + sam.setAffiliateMerkleRoot(address(edition), root); + + _mintOut(edition); + + // Test `buy` to see if it reverts for unapproved affiliate. + bytes32[] memory proof = m.getProof(leaves, 1); + vm.expectRevert(ISAM.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); + } + + function test_samMulticallerSupport(uint256) public { + MulticallerWithSender multicallerWithSender = MulticallerWithSender( + payable(LibMulticaller.MULTICALLER_WITH_SENDER) + ); + vm.etch(LibMulticaller.MULTICALLER_WITH_SENDER, bytes(address(new MulticallerWithSenderUpgradeable()).code)); + MulticallerWithSenderUpgradeable(payable(LibMulticaller.MULTICALLER_WITH_SENDER)).initialize(); + + _testTempVariables memory t; + + (SoundEditionV1_2 edition, MockSAM sam) = _createEditionAndSAM(); + + t.basePrice = _bound(_random(), 1, type(uint96).max); + t.inflectionPrice = _bound(_random(), 1, type(uint128).max); + t.inflectionPoint = _bound(_random(), 1, type(uint32).max); + + address[] memory targets = new address[](3); + targets[0] = address(sam); + targets[1] = address(sam); + targets[2] = address(sam); + + bytes[] memory data = new bytes[](3); + data[0] = abi.encodeWithSelector(ISAM.setBasePrice.selector, address(edition), uint96(t.basePrice)); + data[1] = abi.encodeWithSelector( + ISAM.setInflectionPrice.selector, + address(edition), + uint128(t.inflectionPrice) + ); + data[2] = abi.encodeWithSelector(ISAM.setInflectionPoint.selector, address(edition), uint32(t.inflectionPoint)); + + bool isUnauthorized; + if (_random() % 16 == 0) { + vm.startPrank(address(1)); + vm.expectRevert(Ownable.Unauthorized.selector); + isUnauthorized = true; + } else if (_random() % 2 == 0) { + edition.grantRoles(address(this), edition.ADMIN_ROLE()); + edition.transferOwnership(address(1)); + } + multicallerWithSender.aggregateWithSender(targets, data, new uint256[](data.length)); + if (isUnauthorized) { + return; + } + + SAMInfo memory info = sam.samInfo(address(edition)); + assertEq(info.basePrice, t.basePrice); + 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)); + + multicallerWithSender.aggregateWithSender(targets, data, new uint256[](data.length)); + + vm.deal(address(this), 10 ether); + + _mintOut(edition); + + MulticallerWithSenderAttacker attacker = new MulticallerWithSenderAttacker(); + + address affiliate = address(attacker); + + sam.setAffiliateFee(address(edition), uint16(sam.MAX_AFFILIATE_FEE_BPS())); + + assertTrue(sam.affiliateFeesAccrued(affiliate) == 0); + + sam.buy{ value: 10 ether }(address(edition), address(this), 1, affiliate, new bytes32[](0)); + + assertTrue(sam.affiliateFeesAccrued(affiliate) != 0); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 1; + + // Note that these calls won't revert, due to use of `forceSafeTransferETH`. + if (_random() % 2 == 0) { + sam.withdrawForAffiliate(affiliate); + } else { + targets = new address[](1); + targets[0] = address(sam); + + data = new bytes[](1); + data[0] = abi.encodeWithSelector(ISAM.withdrawForAffiliate.selector, affiliate); + multicallerWithSender.aggregateWithSender(targets, data, new uint256[](data.length)); + } + + // No matter what, the attacker cannot change the affiliate fee. + assertEq(sam.samInfo(address(edition)).affiliateFeeBPS, uint16(sam.MAX_AFFILIATE_FEE_BPS())); + } + + function test_samRequireEditionHasApprovedBytecode() public { + SoundEditionV1_2 edition = createGenericEdition(); + MockSAM sam = new MockSAM(); + + edition.setSAM(address(sam)); + + sam.setCheckEdition(true); + + vm.expectRevert(ISAM.UnapprovedEdition.selector); + sam.create( + address(edition), + BASE_PRICE, + LINEAR_PRICE_SLOPE, + INFLECTION_PRICE, + INFLECTION_POINT, + MAX_SUPPLY, + BUY_FREEZE_TIME, + ARTIST_FEE_BPS, + GOLDEN_EGG_FEE_BPS, + AFFILIATE_FEE_BPS, + address(this), + bytes32(0) + ); + + address[] memory approvedFactories = new address[](1); + approvedFactories[0] = address(soundCreator); + vm.expectEmit(true, true, true, true); + emit ApprovedEditionFactoriesSet(approvedFactories); + sam.setApprovedEditionFactories(approvedFactories); + + sam.create( + address(edition), + BASE_PRICE, + LINEAR_PRICE_SLOPE, + INFLECTION_PRICE, + INFLECTION_POINT, + MAX_SUPPLY, + BUY_FREEZE_TIME, + ARTIST_FEE_BPS, + GOLDEN_EGG_FEE_BPS, + AFFILIATE_FEE_BPS, + address(this), + bytes32(_salt) + ); + + // Check if another clone works. + edition = createGenericEdition(); + sam.create( + address(edition), + BASE_PRICE, + LINEAR_PRICE_SLOPE, + INFLECTION_PRICE, + INFLECTION_POINT, + MAX_SUPPLY, + BUY_FREEZE_TIME, + ARTIST_FEE_BPS, + GOLDEN_EGG_FEE_BPS, + AFFILIATE_FEE_BPS, + address(this), + bytes32(_salt) + ); + + approvedFactories = new address[](0); + vm.expectEmit(true, true, true, true); + emit ApprovedEditionFactoriesSet(approvedFactories); + sam.setApprovedEditionFactories(approvedFactories); + + edition = createGenericEdition(); + + vm.expectRevert(ISAM.UnapprovedEdition.selector); + sam.create( + address(edition), + BASE_PRICE, + LINEAR_PRICE_SLOPE, + INFLECTION_PRICE, + INFLECTION_POINT, + MAX_SUPPLY, + BUY_FREEZE_TIME, + ARTIST_FEE_BPS, + GOLDEN_EGG_FEE_BPS, + AFFILIATE_FEE_BPS, + address(this), + bytes32(_salt) + ); + } +} diff --git a/tests/modules/utils/BondingCurveLib.t.sol b/tests/modules/utils/BondingCurveLib.t.sol new file mode 100644 index 00000000..b47370b0 --- /dev/null +++ b/tests/modules/utils/BondingCurveLib.t.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../../TestPlus.sol"; +import { BondingCurveLib } from "../../../contracts/modules/utils/BondingCurveLib.sol"; +import { LibString } from "solady/utils/LibString.sol"; +import { DynamicBufferLib } from "solady/utils/DynamicBufferLib.sol"; +import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; + +contract BondingCurveLibTest is TestPlus { + using LibString for *; + using DynamicBufferLib for *; + + function testSigmoid2MultiPurchase( + uint32 g, + uint96 h, + uint32 s, + uint8 q + ) public { + vm.assume(s <= type(uint32).max - q); + + uint256 sum; + for (uint256 i = 0; i < q; ++i) { + sum += BondingCurveLib.sigmoid2Sum(g, h, s + uint32(i), 1); + } + uint256 multi = BondingCurveLib.sigmoid2Sum(g, h, s, q); + + assertTrue(multi == sum); + } + + function testSigmoid2MultiSell( + uint32 g, + uint96 h, + uint32 s, + uint8 q + ) public { + vm.assume(s >= q); + + uint256 sum; + for (uint256 i = 0; i < q; ++i) { + sum += BondingCurveLib.sigmoid2Sum(g, h, s - uint32(i + 1), 1); + } + uint256 multi = BondingCurveLib.sigmoid2Sum(g, h, s - q, q); + + assertTrue(multi == sum); + } + + function testSigmoid2(uint32 g, uint96 h) public { + unchecked { + if (g < 3) g = 3; + if (h == 0) h++; + for (uint256 o; o < 8; ++o) { + uint256 supply = g - 3 + o; + if (supply < type(uint32).max) { + uint256 p0 = _sigmoid2(g, h, uint32(supply)); + uint256 p1 = _sigmoid2(g, h, uint32(supply + 1)); + assertTrue(p0 <= p1); + } + } + } + } + + function testSigmoid2() public { + uint32 g; // Inflection point. + uint96 h; // Inflection price. + + g = 1000; + h = 10000000000000000000; + _testSigmoid2Brutalized(g, h, 0, 10000000000000); + _testSigmoid2Brutalized(g, h, 1, 40000000000000); + _testSigmoid2Brutalized(g, h, 2, 90000000000000); + _testSigmoid2Brutalized(g, h, 998, 9980010000000000000); + _testSigmoid2Brutalized(g, h, 999, 10000000000000000000); + _testSigmoid2Brutalized(g, h, 1000, 10000000000000000000); + _testSigmoid2Brutalized(g, h, 1001, 10020000000000000000); + _testSigmoid2Brutalized(g, h, 1002, 10040000000000000000); + _testSigmoid2Brutalized(g, h, 1003, 10060000000000000000); + _testSigmoid2Brutalized(g, h, 9999, 60820000000000000000); + _testSigmoid2Brutalized(g, h, 10000, 60820000000000000000); + _testSigmoid2Brutalized(g, h, 2147483646, 29308580000000000000000); + _testSigmoid2Brutalized(g, h, 2147483647, 29308580000000000000000); + _testSigmoid2Brutalized(g, h, 2147483648, 29308580000000000000000); + _testSigmoid2Brutalized(g, h, 4294967293, 41448600000000000000000); + _testSigmoid2Brutalized(g, h, 4294967294, 41448600000000000000000); + _testSigmoid2Brutalized(g, h, 4294967295, 41448600000000000000000); + + g = 1; + h = 123456789123456789123; + _testSigmoid2Brutalized(g, h, 0, 246913578246913578246); + _testSigmoid2Brutalized(g, h, 1, 246913578246913578246); + _testSigmoid2Brutalized(g, h, 2, 246913578246913578246); + _testSigmoid2Brutalized(g, h, 998, 7654320925654320925626); + _testSigmoid2Brutalized(g, h, 999, 7654320925654320925626); + _testSigmoid2Brutalized(g, h, 1000, 7654320925654320925626); + _testSigmoid2Brutalized(g, h, 1001, 7654320925654320925626); + _testSigmoid2Brutalized(g, h, 1002, 7654320925654320925626); + _testSigmoid2Brutalized(g, h, 1003, 7654320925654320925626); + _testSigmoid2Brutalized(g, h, 9999, 24691357824691357824600); + _testSigmoid2Brutalized(g, h, 10000, 24691357824691357824600); + _testSigmoid2Brutalized(g, h, 2147483646, 11441975215961975215919640); + _testSigmoid2Brutalized(g, h, 2147483647, 11441975215961975215919640); + _testSigmoid2Brutalized(g, h, 2147483648, 11441975215961975215919640); + _testSigmoid2Brutalized(g, h, 4294967293, 16181481350411481350351610); + _testSigmoid2Brutalized(g, h, 4294967294, 16181481350411481350351610); + _testSigmoid2Brutalized(g, h, 4294967295, 16181728263989728263929856); + + g = type(uint32).max; + h = type(uint96).max; + _testSigmoid2Brutalized(g, h, 0, 4294967298); + _testSigmoid2Brutalized(g, h, 1, 17179869192); + _testSigmoid2Brutalized(g, h, 2, 38654705682); + _testSigmoid2Brutalized(g, h, 998, 4286381658371298); + _testSigmoid2Brutalized(g, h, 999, 4294967298000000); + _testSigmoid2Brutalized(g, h, 1000, 4303561527563298); + _testSigmoid2Brutalized(g, h, 1001, 4312164347061192); + _testSigmoid2Brutalized(g, h, 1002, 4320775756493682); + _testSigmoid2Brutalized(g, h, 1003, 4329395755860768); + _testSigmoid2Brutalized(g, h, 9999, 429496729800000000); + _testSigmoid2Brutalized(g, h, 10000, 429582633440927298); + _testSigmoid2Brutalized(g, h, 2147483646, 19807040619342712357236244482); + _testSigmoid2Brutalized(g, h, 2147483647, 19807040637789456435240763392); + _testSigmoid2Brutalized(g, h, 2147483648, 19807040656236200521835216898); + _testSigmoid2Brutalized(g, h, 4294967293, 79228162477370849428944977928); + _testSigmoid2Brutalized(g, h, 4294967294, 79228162495817593515539431422); + _testSigmoid2Brutalized(g, h, 4294967295, 79228162532711081671548469248); + + g = type(uint32).max >> 1; + h = type(uint96).max; + _testSigmoid2Brutalized(g, h, 0, 17179869200); + _testSigmoid2Brutalized(g, h, 1, 68719476800); + _testSigmoid2Brutalized(g, h, 2, 154618822800); + _testSigmoid2Brutalized(g, h, 998, 17145526641469200); + _testSigmoid2Brutalized(g, h, 999, 17179869200000000); + _testSigmoid2Brutalized(g, h, 1000, 17214246118269200); + _testSigmoid2Brutalized(g, h, 1001, 17248657396276800); + _testSigmoid2Brutalized(g, h, 1002, 17283103034022800); + _testSigmoid2Brutalized(g, h, 1003, 17317583031507200); + _testSigmoid2Brutalized(g, h, 9999, 1717986920000000000); + _testSigmoid2Brutalized(g, h, 10000, 1718330534563869200); + _testSigmoid2Brutalized(g, h, 2147483646, 79228162477370849428944977910); + _testSigmoid2Brutalized(g, h, 2147483647, 79228162551157825758142922759); + _testSigmoid2Brutalized(g, h, 2147483648, 79228162624944802087340867607); + _testSigmoid2Brutalized(g, h, 4294967293, 177159557067767033207256239551); + _testSigmoid2Brutalized(g, h, 4294967294, 177159557141554009536454184399); + _testSigmoid2Brutalized(g, h, 4294967295, 177159557141554009536454184399); + + g = 1; + h = type(uint96).max; + _testSigmoid2Brutalized(g, h, 0, 158456325028528675187087900670); + _testSigmoid2Brutalized(g, h, 1, 158456325028528675187087900670); + _testSigmoid2Brutalized(g, h, 2, 158456325028528675187087900670); + _testSigmoid2Brutalized(g, h, 998, 4912146075884388930799724920770); + _testSigmoid2Brutalized(g, h, 999, 4912146075884388930799724920770); + _testSigmoid2Brutalized(g, h, 1000, 4912146075884388930799724920770); + _testSigmoid2Brutalized(g, h, 1001, 4912146075884388930799724920770); + _testSigmoid2Brutalized(g, h, 1002, 4912146075884388930799724920770); + _testSigmoid2Brutalized(g, h, 1003, 4912146075884388930799724920770); + _testSigmoid2Brutalized(g, h, 9999, 15845632502852867518708790067000); + _testSigmoid2Brutalized(g, h, 10000, 15845632502852867518708790067000); + _testSigmoid2Brutalized(g, h, 2147483646, 7342866101822018808169653317047800); + _testSigmoid2Brutalized(g, h, 2147483647, 7342866101822018808169653317047800); + _testSigmoid2Brutalized(g, h, 2147483648, 7342866101822018808169653317047800); + _testSigmoid2Brutalized(g, h, 4294967293, 10384435260744626728385805570408450); + _testSigmoid2Brutalized(g, h, 4294967294, 10384435260744626728385805570408450); + _testSigmoid2Brutalized(g, h, 4294967295, 10384593717069655257060992658309120); + + // Yes, for certain values, where the inflection price is too low compared to + // the inflection point, the price for the quadratic region can be zero. + g = 1351215609; + h = 4294967296; + _testSigmoid2Brutalized(g, h, 0, 0); + _testSigmoid2Brutalized(g, h, 1, 0); + _testSigmoid2Brutalized(g, h, 2, 0); + _testSigmoid2Brutalized(g, h, 998, 0); + _testSigmoid2Brutalized(g, h, 999, 0); + _testSigmoid2Brutalized(g, h, 1000, 0); + _testSigmoid2Brutalized(g, h, 1001, 0); + _testSigmoid2Brutalized(g, h, 1002, 0); + _testSigmoid2Brutalized(g, h, 1003, 0); + _testSigmoid2Brutalized(g, h, 9999, 0); + _testSigmoid2Brutalized(g, h, 10000, 0); + _testSigmoid2Brutalized(g, h, 2147483646, 7869512557); + _testSigmoid2Brutalized(g, h, 2147483647, 7869512557); + _testSigmoid2Brutalized(g, h, 2147483648, 7869512563); + _testSigmoid2Brutalized(g, h, 4294967293, 13386511417); + _testSigmoid2Brutalized(g, h, 4294967294, 13386511417); + _testSigmoid2Brutalized(g, h, 4294967295, 13386511423); + + g = 0; + h = 123456789123456789123; + _testSigmoid2Brutalized(g, h, 0, 0); + _testSigmoid2Brutalized(g, h, 4294967295, 0); + + g = 0; + h = 0; + _testSigmoid2Brutalized(g, h, 0, 0); + _testSigmoid2Brutalized(g, h, 4294967295, 0); + + g = 1000; + h = 0; + _testSigmoid2Brutalized(g, h, 0, 0); + _testSigmoid2Brutalized(g, h, 4294967295, 0); + } + + // Uncomment this to run. + + // function testSigmoid2FFI(uint32 g, uint96 h, uint32 s) public { + // DynamicBufferLib.DynamicBuffer memory b; + // b.append("import math;"); + // b.append("g = ", bytes(LibString.toString(g)), ";"); + // b.append("h = ", bytes(LibString.toString(h)), ";"); + // b.append("s = ", bytes(LibString.toString(s)), ";"); + // b.append("print (str(0 if (g == 0 or h == 0) else ("); + // b.append( + // "int( ((h * int(math.isqrt(abs((s + 1) - ((3 * g) >> 2)) * g))) << 1) // g) ", + // "if (s + 1) >= g else ", + // "int((s + 1) * (s + 1) * (h // (g * g)))" + // ); + // b.append(")) + '_');"); + + // string[] memory cmds = new string[](3); + // cmds[0] = "python3"; + // cmds[1] = "-c"; + // cmds[2] = string(b.data); + + // bytes memory result = vm.ffi(cmds); + + // uint256 computed = _sigmoid2(g, h, s); + + // assertEq(LibString.toString(computed).concat("_"), string(result)); + // } + + function _testSigmoid2Brutalized( + uint32 inflectionPoint, + uint96 inflectionPrice, + uint32 supply, + uint256 expectedResult + ) internal { + uint256 w = _random(); + assembly { + inflectionPoint := or(inflectionPoint, shl(32, w)) + inflectionPrice := or(inflectionPrice, shl(96, w)) + supply := or(supply, shl(32, w)) + } + assertEq(_sigmoid2(inflectionPoint, inflectionPrice, supply), expectedResult); + } + + function _sigmoid2( + uint32 inflectionPoint, + uint96 inflectionPrice, + uint32 supply + ) internal pure returns (uint256) { + return BondingCurveLib.sigmoid2Sum(inflectionPoint, inflectionPrice, supply, 1); + } + + function testSigmoid2Sum( + uint128 inflectionPrice, + uint32 fromSupply, + uint32 quantity0, + uint32 quantity1 + ) public { + if (uint256(fromSupply) + uint256(quantity0) + uint256(quantity1) > type(uint32).max) { + fromSupply = uint32(_bound(_random(), 0, type(uint32).max)); + quantity0 = uint32(_bound(_random(), 0, type(uint32).max - fromSupply)); + quantity1 = uint32(_bound(_random(), 0, type(uint32).max - fromSupply - quantity0)); + } + uint32 inflectionPoint = type(uint32).max; + uint256 sum0 = BondingCurveLib.sigmoid2Sum(inflectionPoint, inflectionPrice, fromSupply, quantity0); + uint256 sum1 = BondingCurveLib.sigmoid2Sum(inflectionPoint, inflectionPrice, fromSupply + quantity0, quantity1); + assertEq( + sum0 + sum1, + BondingCurveLib.sigmoid2Sum(inflectionPoint, inflectionPrice, fromSupply, quantity0 + quantity1) + ); + } + + function testSigmoid2Sum( + uint128 inflectionPrice, + uint32 fromSupply, + uint32 quantity + ) public { + uint32 inflectionPoint = uint32(type(uint32).max); + quantity = uint32(_bound(quantity, 0, 256)); + uint256 sum = BondingCurveLib.sigmoid2Sum(inflectionPoint, inflectionPrice, fromSupply, quantity); + assertEq(sum, _sigmoid2Sum(inflectionPoint, inflectionPrice, fromSupply, quantity)); + } + + function _sigmoid2Sum( + uint32 inflectionPoint, + uint128 inflectionPrice, + uint32 fromSupply, + uint32 quantity + ) internal pure returns (uint256 sum) { + uint256 g = inflectionPoint; + uint256 h = inflectionPrice; + + // Early return to save gas if either `g` or `h` is zero. + if (g * h == 0) return 0; + + uint256 s = uint256(fromSupply) + 1; + uint256 end = s + uint256(quantity); + uint256 quadraticEnd = FixedPointMathLib.min(g, end); + + if (s < quadraticEnd) { + uint256 a = FixedPointMathLib.rawDiv(h, g * g); + do { + sum += s * s * a; + } while (++s != quadraticEnd); + } + + if (s < end) { + uint256 c = (3 * g) >> 2; + uint256 h2 = h << 1; + do { + uint256 r = FixedPointMathLib.sqrt((s - c) * g); + sum += FixedPointMathLib.rawDiv(h2 * r, g); + } while (++s != end); + } + } + + function testLinearSum( + uint128 linearPriceSlope, + uint32 fromSupply, + uint32 quantity + ) public { + quantity = uint32(_bound(quantity, 0, 256)); + uint256 sum = BondingCurveLib.linearSum(linearPriceSlope, fromSupply, quantity); + assertEq(sum, _linearSum(linearPriceSlope, fromSupply, quantity)); + } + + function testLinearSum( + uint128 linearPriceSlope, + uint32 fromSupply, + uint32 quantity0, + uint32 quantity1 + ) public { + if (uint256(fromSupply) + uint256(quantity0) + uint256(quantity1) > type(uint32).max) { + fromSupply = uint32(_bound(_random(), 0, type(uint32).max)); + quantity0 = uint32(_bound(_random(), 0, type(uint32).max - fromSupply)); + quantity1 = uint32(_bound(_random(), 0, type(uint32).max - fromSupply - quantity0)); + } + uint256 sum0 = BondingCurveLib.linearSum(linearPriceSlope, fromSupply, quantity0); + uint256 sum1 = BondingCurveLib.linearSum(linearPriceSlope, fromSupply + quantity0, quantity1); + assertEq(sum0 + sum1, BondingCurveLib.linearSum(linearPriceSlope, fromSupply, quantity0 + quantity1)); + } + + function _linearSum( + uint128 linearPriceSlope, + uint32 fromSupply, + uint32 quantity + ) internal pure returns (uint256 sum) { + uint256 m = linearPriceSlope; + + // Early return to save gas if `m` is zero. + if (m == 0) return 0; + + uint256 s = uint256(fromSupply) + 1; + uint256 end = s + uint256(quantity); + + while (s < end) { + sum += m * s; + ++s; + } + } +}