From bcdd5be1e0996ee54bd6d20fbff6a9a6a7b584dd Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 17:12:35 -0500 Subject: [PATCH 01/26] new contracts Signed-off-by: Adam Wolf --- .../erc721m/ERC721CMInitializableV1_0_1.sol | 545 ++++++ .../erc721m/ERC721MInitializableV1_0_1.sol | 578 ++++++ .../nft/erc721m/clones/ERC721ACloneable.sol | 1585 +++++++++++++++++ .../ERC721AConduitPreapprovedCloneable.sol | 33 + .../clones/ERC721AQueryableCloneable.sol | 233 +++ .../ERC721MagicDropMetadataCloneable.sol | 174 ++ .../interfaces/IERC721MagicDropMetadata.sol | 133 ++ test/erc721m/ERC721MInitializableTest.t.sol | 100 ++ 8 files changed, 3381 insertions(+) create mode 100644 contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol create mode 100644 contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol create mode 100644 contracts/nft/erc721m/clones/ERC721ACloneable.sol create mode 100644 contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol create mode 100644 contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol create mode 100644 contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol create mode 100644 contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol create mode 100644 test/erc721m/ERC721MInitializableTest.t.sol diff --git a/contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol b/contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol new file mode 100644 index 0000000..caf1489 --- /dev/null +++ b/contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol @@ -0,0 +1,545 @@ +//SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {ERC2981} from "solady/src/tokens/ERC2981.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; +import { + ERC721ACQueryableInitializable, + ERC721AUpgradeable, + IERC721AUpgradeable +} from "../creator-token-standards/ERC721ACQueryableInitializable.sol"; +import {ERC721MStorage} from "./ERC721MStorage.sol"; +import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; +import {MintStageInfo} from "../../common/Structs.sol"; +import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; +import {Cosignable} from "../../common/Cosignable.sol"; +import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; + +/** + * @title ERC721CMInitializableV1_0_1 + * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this + * contract is for use with EIP-1167 Minimal Proxies (Clones). + */ +contract ERC721CMInitializableV1_0_1 is + IERC721MInitializable, + ERC721ACQueryableInitializable, + ERC2981, + Ownable, + ReentrancyGuard, + Cosignable, + AuthorizedMinterControl, + ERC721MStorage +{ + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract + /// @param name The name of the token collection + /// @param symbol The symbol of the token collection + /// @param initialOwner The address of the initial owner + function initialize(string calldata name, string calldata symbol, address initialOwner) + external + initializer + initializerERC721A + { + if (initialOwner == address(0)) { + revert InitialOwnerCannotBeZero(); + } + + __ERC721ACQueryableInitializable_init(name, symbol); + _initializeOwner(initialOwner); + } + + /*============================================================== + = META = + ==============================================================*/ + + /// @notice Returns the contract name and version + /// @return The contract name and version as strings + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("ERC721CMInitializable", "1.0.1"); + } + + /// @notice Gets the token URI for a specific token ID + /// @param tokenId The ID of the token + /// @return The token URI + function tokenURI(uint256 tokenId) + public + view + override(ERC721AUpgradeable, IERC721AUpgradeable) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _currentBaseURI; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; + } + + /// @notice Gets the contract URI + /// @return The contract URI + function contractURI() public view returns (string memory) { + return _contractURI; + } + + /*============================================================== + = MODIFIERS = + ==============================================================*/ + + /// @notice Modifier to check if the contract is mintable + modifier canMint() { + if (!_mintable) revert NotMintable(); + _; + } + + /// @notice Modifier to check if the total supply is enough + /// @param qty The quantity to mint + modifier hasSupply(uint256 qty) { + if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); + _; + } + + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + + /// @notice Mints tokens for the caller + /// @param qty The quantity to mint + /// @param limit The minting limit for the caller (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) + external + payable + virtual + nonReentrant + { + _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); + } + + /// @notice Allows authorized minters to mint tokens for a specified address + /// @param to The address to mint tokens for + /// @param qty The quantity to mint + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function authorizedMint( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) external payable onlyAuthorizedMinter { + _mintInternal(qty, to, limit, proof, timestamp, signature); + } + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Gets the stage info for a given stage index + /// @param index The stage index + /// @return The stage info, wallet minted count, and stage minted count + function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { + if (index >= _mintStages.length) { + revert InvalidStage(); + } + uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; + uint256 stageMinted = _stageMintedCounts[index]; + return (_mintStages[index], walletMinted, stageMinted); + } + + /// @notice Gets the mint currency address + /// @return The address of the mint currency + function getMintCurrency() external view returns (address) { + return _mintCurrency; + } + + /// @notice Gets the cosign nonce for a specific minter + /// @param minter The address of the minter + /// @return The cosign nonce + function getCosignNonce(address minter) public view returns (uint256) { + return _numberMinted(minter); + } + + /// @notice Gets the mintable status + /// @return The mintable status + function getMintable() external view returns (bool) { + return _mintable; + } + + /// @notice Gets the number of minting stages + /// @return The number of minting stages + function getNumberStages() external view override returns (uint256) { + return _mintStages.length; + } + + /// @notice Gets the maximum mintable supply + /// @return The maximum mintable supply + function getMaxMintableSupply() external view override returns (uint256) { + return _maxMintableSupply; + } + + /// @notice Gets the global wallet limit + /// @return The global wallet limit + function getGlobalWalletLimit() external view override returns (uint256) { + return _globalWalletLimit; + } + + /// @notice Gets the total minted count for a specific address + /// @param a The address to get the minted count for + /// @return The total minted count + function totalMintedByAddress(address a) external view virtual override returns (uint256) { + return _numberMinted(a); + } + + /// @notice Gets the active stage from the timestamp + /// @param timestamp The timestamp to get the active stage from + /// @return The active stage + function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { + for (uint256 i = 0; i < _mintStages.length; i++) { + if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { + return i; + } + } + revert InvalidStage(); + } + + /// @notice Checks if the contract supports a given interface + /// @param interfaceId The interface identifier + /// @return True if the contract supports the interface, false otherwise + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC2981, IERC721AUpgradeable, ERC721ACQueryableInitializable) + returns (bool) + { + return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) + || ERC721ACQueryableInitializable.supportsInterface(interfaceId); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets up the contract with initial parameters + /// @param baseURI The base URI for the token URIs + /// @param tokenURISuffix The suffix for the token URIs + /// @param maxMintableSupply The maximum mintable supply + /// @param globalWalletLimit The global wallet limit + /// @param mintCurrency The address of the mint currency + /// @param fundReceiver The address to receive funds + /// @param initialStages The initial mint stages + /// @param royaltyReceiver The address to receive royalties + /// @param royaltyFeeNumerator The royalty fee numerator + function setup( + string calldata baseURI, + string calldata tokenURISuffix, + uint256 maxMintableSupply, + uint256 globalWalletLimit, + address mintCurrency, + address fundReceiver, + MintStageInfo[] calldata initialStages, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) external onlyOwner { + if (globalWalletLimit > maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _mintable = true; + _maxMintableSupply = maxMintableSupply; + _globalWalletLimit = globalWalletLimit; + _mintCurrency = mintCurrency; + _fundReceiver = fundReceiver; + _currentBaseURI = baseURI; + _tokenURISuffix = tokenURISuffix; + _setTimestampExpirySeconds(300); // 5 minutes + + if (initialStages.length > 0) { + _setStages(initialStages); + } + + if (royaltyReceiver != address(0)) { + setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + } + } + + /// @notice Adds an authorized minter + /// @param minter The address to add as an authorized minter + function addAuthorizedMinter(address minter) external override onlyOwner { + _addAuthorizedMinter(minter); + } + + /// @notice Removes an authorized minter + /// @param minter The address to remove as an authorized minter + function removeAuthorizedMinter(address minter) external override onlyOwner { + _removeAuthorizedMinter(minter); + } + + /// @notice Sets the cosigner address + /// @param cosigner The address to set as the cosigner + function setCosigner(address cosigner) external override onlyOwner { + _setCosigner(cosigner); + } + + /// @notice Sets the timestamp expiry seconds + /// @param timestampExpirySeconds The expiry time in seconds for timestamps + function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { + _setTimestampExpirySeconds(timestampExpirySeconds); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function setStages(MintStageInfo[] calldata newStages) external onlyOwner { + _setStages(newStages); + } + + /// @notice Sets the mintable status + /// @param mintable The mintable status to set + function setMintable(bool mintable) external onlyOwner { + _mintable = mintable; + emit SetMintable(mintable); + } + + /// @notice Sets the default royalty for the contract + /// @param receiver The address to receive royalties + /// @param feeNumerator The royalty fee numerator + function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { + super._setDefaultRoyalty(receiver, feeNumerator); + emit DefaultRoyaltySet(receiver, feeNumerator); + } + + /// @notice Sets the maximum mintable supply + /// @param maxMintableSupply The maximum mintable supply to set + function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { + if (maxMintableSupply > _maxMintableSupply) { + revert CannotIncreaseMaxMintableSupply(); + } + _maxMintableSupply = maxMintableSupply; + emit SetMaxMintableSupply(maxMintableSupply); + } + + /// @notice Sets the global wallet limit + /// @param globalWalletLimit The global wallet limit to set + function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { + if (globalWalletLimit > _maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _globalWalletLimit = globalWalletLimit; + emit SetGlobalWalletLimit(globalWalletLimit); + } + + /// @notice Allows the owner to mint tokens for a specific address + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { + _safeMint(to, qty); + } + + /// @notice Withdraws the total mint fee and remaining balance from the contract + /// @dev Can only be called by the owner + function withdraw() external onlyOwner { + (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); + if (!success) revert TransferFailed(); + _totalMintFee = 0; + + uint256 remainingValue = address(this).balance; + (success,) = _fundReceiver.call{value: remainingValue}(""); + if (!success) revert WithdrawFailed(); + + emit Withdraw(_totalMintFee + remainingValue); + } + + /// @notice Withdraws ERC20 tokens from the contract + /// @dev Can only be called by the owner + function withdrawERC20() external onlyOwner { + if (_mintCurrency == address(0)) revert WrongMintCurrency(); + + uint256 totalFee = _totalMintFee; + uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); + + if (remaining < totalFee) revert InsufficientBalance(); + + _totalMintFee = 0; + uint256 totalAmount = totalFee + remaining; + + SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); + SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); + + emit WithdrawERC20(_mintCurrency, totalAmount); + } + + /// @notice Sets the base URI for the token URIs + /// @param baseURI The base URI to set + function setBaseURI(string calldata baseURI) external onlyOwner { + _currentBaseURI = baseURI; + emit SetBaseURI(baseURI); + } + + /// @notice Sets the token URI suffix + /// @param suffix The suffix to set + function setTokenURISuffix(string calldata suffix) external onlyOwner { + _tokenURISuffix = suffix; + } + + /// @notice Sets the contract URI + /// @param uri The URI to set + function setContractURI(string calldata uri) external onlyOwner { + _contractURI = uri; + emit SetContractURI(uri); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Internal function to handle minting logic + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function _mintInternal( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) internal canMint hasSupply(qty) { + uint256 stageTimestamp = block.timestamp; + bool waiveMintFee = false; + + if (getCosigner() != address(0)) { + waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); + _assertValidTimestamp(timestamp); + stageTimestamp = timestamp; + } + + uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); + MintStageInfo memory stage = _mintStages[activeStage]; + + uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; + + // Check value if minting with ETH + if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); + + // Check stage supply if applicable + if (stage.maxStageSupply > 0) { + if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { + revert StageSupplyExceeded(); + } + } + + // Check global wallet limit if applicable + if (_globalWalletLimit > 0) { + if (_numberMinted(to) + qty > _globalWalletLimit) { + revert WalletGlobalLimitExceeded(); + } + } + + // Check wallet limit for stage if applicable, limit == 0 means no limit enforced + if (stage.walletLimit > 0) { + if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { + revert WalletStageLimitExceeded(); + } + } + + // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required + if (stage.merkleRoot != 0) { + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { + revert InvalidProof(); + } + + // Verify merkle proof mint limit + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + revert WalletStageLimitExceeded(); + } + } + + if (_mintCurrency != address(0)) { + // ERC20 mint payment + SafeTransferLib.safeTransferFrom( + _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty + ); + } + + _totalMintFee += adjustedMintFee * qty; + + _stageMintedCountsPerWallet[activeStage][to] += qty; + _stageMintedCounts[activeStage] += qty; + _safeMint(to, qty); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function _setStages(MintStageInfo[] calldata newStages) internal { + delete _mintStages; + + for (uint256 i = 0; i < newStages.length;) { + if (i >= 1) { + if ( + newStages[i].startTimeUnixSeconds + < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() + ) { + revert InsufficientStageTimeGap(); + } + } + _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); + _mintStages.push( + MintStageInfo({ + price: newStages[i].price, + mintFee: newStages[i].mintFee, + walletLimit: newStages[i].walletLimit, + merkleRoot: newStages[i].merkleRoot, + maxStageSupply: newStages[i].maxStageSupply, + startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, + endTimeUnixSeconds: newStages[i].endTimeUnixSeconds + }) + ); + emit UpdateStage( + i, + newStages[i].price, + newStages[i].mintFee, + newStages[i].walletLimit, + newStages[i].merkleRoot, + newStages[i].maxStageSupply, + newStages[i].startTimeUnixSeconds, + newStages[i].endTimeUnixSeconds + ); + + unchecked { + ++i; + } + } + } + + /// @notice Validates the start and end timestamps for a stage + /// @param start The start timestamp + /// @param end The end timestamp + function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { + if (start >= end) revert InvalidStartAndEndTimestamp(); + } + + /// @notice Requires the caller to be the contract owner + function _requireCallerIsContractOwner() internal view override { + return _checkOwner(); + } + + /// @dev Overriden to prevent double-initialization of the owner. + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } +} diff --git a/contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol b/contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol new file mode 100644 index 0000000..38d74f4 --- /dev/null +++ b/contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol @@ -0,0 +1,578 @@ +//SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {ERC2981} from "solady/src/tokens/ERC2981.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; +import {Initializable} from "solady/src/utils/Initializable.sol"; + +import {ERC721AUpgradeable, IERC721AUpgradeable} from "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; +import { + ERC721AQueryableUpgradeable, + IERC721AQueryableUpgradeable +} from "erc721a-upgradeable/contracts/extensions/ERC721AQueryableUpgradeable.sol"; +import {IERC721A, ERC721A} from "erc721a/contracts/extensions/ERC721AQueryable.sol"; +import {ERC721MStorage} from "./ERC721MStorage.sol"; +import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; +import {MintStageInfo} from "../../common/Structs.sol"; +import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; +import {Cosignable} from "../../common/Cosignable.sol"; +import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; + +/** + * @title ERC721MInitializableV1_0_1 + * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this + * contract is for use with EIP-1167 Minimal Proxies (Clones). + */ +contract ERC721MInitializableV1_0_1 is + IERC721MInitializable, + ERC721AQueryableUpgradeable, + ERC2981, + Ownable, + ReentrancyGuard, + Cosignable, + AuthorizedMinterControl, + ERC721MStorage, + Initializable +{ + /*============================================================== + = STORAGE = + ==============================================================*/ + bool public frozen; + + /*============================================================== + = EVENTS = + ==============================================================*/ + event SetFrozen(bool frozen); + + /*============================================================== + = ERRORS = + ==============================================================*/ + error TransfersAreFrozen(); + + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract + /// @param name The name of the token collection + /// @param symbol The symbol of the token collection + /// @param initialOwner The address of the initial owner + function initialize(string calldata name, string calldata symbol, address initialOwner) + external + initializer + initializerERC721A + { + if (initialOwner == address(0)) { + revert InitialOwnerCannotBeZero(); + } + + __ERC721A_init_unchained(name, symbol); + __ERC721AQueryable_init_unchained(); + _initializeOwner(initialOwner); + } + + /*============================================================== + = META = + ==============================================================*/ + + /// @notice Returns the contract name and version + /// @return The contract name and version as strings + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("ERC721CMInitializable", "1.0.1"); + } + + /// @notice Gets the token URI for a specific token ID + /// @param tokenId The ID of the token + /// @return The token URI + function tokenURI(uint256 tokenId) + public + view + override(ERC721AUpgradeable, IERC721AUpgradeable) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _currentBaseURI; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; + } + + /// @notice Gets the contract URI + /// @return The contract URI + function contractURI() public view returns (string memory) { + return _contractURI; + } + + /*============================================================== + = MODIFIERS = + ==============================================================*/ + + /// @notice Modifier to check if the contract is mintable + modifier canMint() { + if (!_mintable) revert NotMintable(); + _; + } + + /// @notice Modifier to check if the total supply is enough + /// @param qty The quantity to mint + modifier hasSupply(uint256 qty) { + if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); + _; + } + + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + + /// @notice Mints tokens for the caller + /// @param qty The quantity to mint + /// @param limit The minting limit for the caller (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) + external + payable + virtual + nonReentrant + { + _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); + } + + /// @notice Allows authorized minters to mint tokens for a specified address + /// @param to The address to mint tokens for + /// @param qty The quantity to mint + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function authorizedMint( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) external payable onlyAuthorizedMinter { + _mintInternal(qty, to, limit, proof, timestamp, signature); + } + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Gets the stage info for a given stage index + /// @param index The stage index + /// @return The stage info, wallet minted count, and stage minted count + function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { + if (index >= _mintStages.length) { + revert InvalidStage(); + } + uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; + uint256 stageMinted = _stageMintedCounts[index]; + return (_mintStages[index], walletMinted, stageMinted); + } + + /// @notice Gets the mint currency address + /// @return The address of the mint currency + function getMintCurrency() external view returns (address) { + return _mintCurrency; + } + + /// @notice Gets the cosign nonce for a specific minter + /// @param minter The address of the minter + /// @return The cosign nonce + function getCosignNonce(address minter) public view returns (uint256) { + return _numberMinted(minter); + } + + /// @notice Gets the mintable status + /// @return The mintable status + function getMintable() external view returns (bool) { + return _mintable; + } + + /// @notice Gets the number of minting stages + /// @return The number of minting stages + function getNumberStages() external view override returns (uint256) { + return _mintStages.length; + } + + /// @notice Gets the maximum mintable supply + /// @return The maximum mintable supply + function getMaxMintableSupply() external view override returns (uint256) { + return _maxMintableSupply; + } + + /// @notice Gets the global wallet limit + /// @return The global wallet limit + function getGlobalWalletLimit() external view override returns (uint256) { + return _globalWalletLimit; + } + + /// @notice Gets the total minted count for a specific address + /// @param a The address to get the minted count for + /// @return The total minted count + function totalMintedByAddress(address a) external view virtual override returns (uint256) { + return _numberMinted(a); + } + + /// @notice Gets the active stage from the timestamp + /// @param timestamp The timestamp to get the active stage from + /// @return The active stage + function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { + for (uint256 i = 0; i < _mintStages.length; i++) { + if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { + return i; + } + } + revert InvalidStage(); + } + + /// @notice Checks if the contract supports a given interface + /// @param interfaceId The interface identifier + /// @return True if the contract supports the interface, false otherwise + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC2981, ERC721AUpgradeable, IERC721AUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) + || ERC721AUpgradeable.supportsInterface(interfaceId); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets up the contract with initial parameters + /// @param baseURI The base URI for the token URIs + /// @param tokenURISuffix The suffix for the token URIs + /// @param maxMintableSupply The maximum mintable supply + /// @param globalWalletLimit The global wallet limit + /// @param mintCurrency The address of the mint currency + /// @param fundReceiver The address to receive funds + /// @param initialStages The initial mint stages + /// @param royaltyReceiver The address to receive royalties + /// @param royaltyFeeNumerator The royalty fee numerator + function setup( + string calldata baseURI, + string calldata tokenURISuffix, + uint256 maxMintableSupply, + uint256 globalWalletLimit, + address mintCurrency, + address fundReceiver, + MintStageInfo[] calldata initialStages, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) external onlyOwner { + if (globalWalletLimit > maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _mintable = true; + _maxMintableSupply = maxMintableSupply; + _globalWalletLimit = globalWalletLimit; + _mintCurrency = mintCurrency; + _fundReceiver = fundReceiver; + _currentBaseURI = baseURI; + _tokenURISuffix = tokenURISuffix; + _setTimestampExpirySeconds(300); // 5 minutes + + if (initialStages.length > 0) { + _setStages(initialStages); + } + + if (royaltyReceiver != address(0)) { + setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + } + } + + /// @notice Adds an authorized minter + /// @param minter The address to add as an authorized minter + function addAuthorizedMinter(address minter) external override onlyOwner { + _addAuthorizedMinter(minter); + } + + /// @notice Removes an authorized minter + /// @param minter The address to remove as an authorized minter + function removeAuthorizedMinter(address minter) external override onlyOwner { + _removeAuthorizedMinter(minter); + } + + /// @notice Sets the cosigner address + /// @param cosigner The address to set as the cosigner + function setCosigner(address cosigner) external override onlyOwner { + _setCosigner(cosigner); + } + + /// @notice Sets the timestamp expiry seconds + /// @param timestampExpirySeconds The expiry time in seconds for timestamps + function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { + _setTimestampExpirySeconds(timestampExpirySeconds); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function setStages(MintStageInfo[] calldata newStages) external onlyOwner { + _setStages(newStages); + } + + /// @notice Sets the mintable status + /// @param mintable The mintable status to set + function setMintable(bool mintable) external onlyOwner { + _mintable = mintable; + emit SetMintable(mintable); + } + + /// @notice Sets the default royalty for the contract + /// @param receiver The address to receive royalties + /// @param feeNumerator The royalty fee numerator + function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { + super._setDefaultRoyalty(receiver, feeNumerator); + emit DefaultRoyaltySet(receiver, feeNumerator); + } + + /// @notice Sets the maximum mintable supply + /// @param maxMintableSupply The maximum mintable supply to set + function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { + if (maxMintableSupply > _maxMintableSupply) { + revert CannotIncreaseMaxMintableSupply(); + } + _maxMintableSupply = maxMintableSupply; + emit SetMaxMintableSupply(maxMintableSupply); + } + + /// @notice Sets the global wallet limit + /// @param globalWalletLimit The global wallet limit to set + function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { + if (globalWalletLimit > _maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _globalWalletLimit = globalWalletLimit; + emit SetGlobalWalletLimit(globalWalletLimit); + } + + /// @notice Allows the owner to mint tokens for a specific address + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { + _safeMint(to, qty); + } + + /// @notice Withdraws the total mint fee and remaining balance from the contract + /// @dev Can only be called by the owner + function withdraw() external onlyOwner { + (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); + if (!success) revert TransferFailed(); + _totalMintFee = 0; + + uint256 remainingValue = address(this).balance; + (success,) = _fundReceiver.call{value: remainingValue}(""); + if (!success) revert WithdrawFailed(); + + emit Withdraw(_totalMintFee + remainingValue); + } + + /// @notice Withdraws ERC20 tokens from the contract + /// @dev Can only be called by the owner + function withdrawERC20() external onlyOwner { + if (_mintCurrency == address(0)) revert WrongMintCurrency(); + + uint256 totalFee = _totalMintFee; + uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); + + if (remaining < totalFee) revert InsufficientBalance(); + + _totalMintFee = 0; + uint256 totalAmount = totalFee + remaining; + + SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); + SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); + + emit WithdrawERC20(_mintCurrency, totalAmount); + } + + /// @notice Sets the base URI for the token URIs + /// @param baseURI The base URI to set + function setBaseURI(string calldata baseURI) external onlyOwner { + _currentBaseURI = baseURI; + emit SetBaseURI(baseURI); + } + + /// @notice Sets the token URI suffix + /// @param suffix The suffix to set + function setTokenURISuffix(string calldata suffix) external onlyOwner { + _tokenURISuffix = suffix; + } + + /// @notice Sets the contract URI + /// @param uri The URI to set + function setContractURI(string calldata uri) external onlyOwner { + _contractURI = uri; + emit SetContractURI(uri); + } + + /// @notice Sets the frozen status + /// @param _frozen The frozen status to set + function setFrozen(bool _frozen) external onlyOwner { + frozen = _frozen; + emit SetFrozen(_frozen); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Internal function to handle minting logic + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function _mintInternal( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) internal canMint hasSupply(qty) { + uint256 stageTimestamp = block.timestamp; + bool waiveMintFee = false; + + if (getCosigner() != address(0)) { + waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); + _assertValidTimestamp(timestamp); + stageTimestamp = timestamp; + } + + uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); + MintStageInfo memory stage = _mintStages[activeStage]; + + uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; + + // Check value if minting with ETH + if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); + + // Check stage supply if applicable + if (stage.maxStageSupply > 0) { + if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { + revert StageSupplyExceeded(); + } + } + + // Check global wallet limit if applicable + if (_globalWalletLimit > 0) { + if (_numberMinted(to) + qty > _globalWalletLimit) { + revert WalletGlobalLimitExceeded(); + } + } + + // Check wallet limit for stage if applicable, limit == 0 means no limit enforced + if (stage.walletLimit > 0) { + if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { + revert WalletStageLimitExceeded(); + } + } + + // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required + if (stage.merkleRoot != 0) { + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { + revert InvalidProof(); + } + + // Verify merkle proof mint limit + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + revert WalletStageLimitExceeded(); + } + } + + if (_mintCurrency != address(0)) { + // ERC20 mint payment + SafeTransferLib.safeTransferFrom( + _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty + ); + } + + _totalMintFee += adjustedMintFee * qty; + + _stageMintedCountsPerWallet[activeStage][to] += qty; + _stageMintedCounts[activeStage] += qty; + _safeMint(to, qty); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function _setStages(MintStageInfo[] calldata newStages) internal { + delete _mintStages; + + for (uint256 i = 0; i < newStages.length;) { + if (i >= 1) { + if ( + newStages[i].startTimeUnixSeconds + < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() + ) { + revert InsufficientStageTimeGap(); + } + } + _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); + _mintStages.push( + MintStageInfo({ + price: newStages[i].price, + mintFee: newStages[i].mintFee, + walletLimit: newStages[i].walletLimit, + merkleRoot: newStages[i].merkleRoot, + maxStageSupply: newStages[i].maxStageSupply, + startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, + endTimeUnixSeconds: newStages[i].endTimeUnixSeconds + }) + ); + emit UpdateStage( + i, + newStages[i].price, + newStages[i].mintFee, + newStages[i].walletLimit, + newStages[i].merkleRoot, + newStages[i].maxStageSupply, + newStages[i].startTimeUnixSeconds, + newStages[i].endTimeUnixSeconds + ); + + unchecked { + ++i; + } + } + } + + /// @notice Blocks token transfers when the contract is frozen + /// @dev Overrides the _beforeTokenTransfers function from ERC721A + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) + internal + virtual + override + { + if (frozen && from != address(0)) revert TransfersAreFrozen(); + super._beforeTokenTransfers(from, to, startTokenId, quantity); + } + + /// @notice Validates the start and end timestamps for a stage + /// @param start The start timestamp + /// @param end The end timestamp + function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { + if (start >= end) revert InvalidStartAndEndTimestamp(); + } + + /// @dev Overriden to prevent double-initialization of the owner. + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } +} diff --git a/contracts/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol new file mode 100644 index 0000000..aaabe91 --- /dev/null +++ b/contracts/nft/erc721m/clones/ERC721ACloneable.sol @@ -0,0 +1,1585 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.3.0 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import {IERC721A} from "erc721a/contracts/IERC721A.sol"; + +import {Initializable} from "solady/src/utils/Initializable.sol"; + +/** + * @dev Interface of ERC721 token receiver. + */ +interface ERC721A__IERC721Receiver { + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); +} + +/** + * @title ERC721ACloneable + * + * @dev Implementation of the [ERC721](https://eips.ethereum.org/EIPS/eip-721) + * Non-Fungible Token Standard, including the Metadata extension. + * Optimized for lower gas during batch mints. + * + * Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...) + * starting from `_startTokenId()`. + * + * The `_sequentialUpTo()` function can be overriden to enable spot mints + * (i.e. non-consecutive mints) for `tokenId`s greater than `_sequentialUpTo()`. + * + * Assumptions: + * + * - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply. + * - The maximum token ID cannot exceed 2**256 - 1 (max value of uint256). + */ +contract ERC721ACloneable is IERC721A, Initializable { + // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). + struct TokenApprovalRef { + address value; + } + + // ============================================================= + // CONSTANTS + // ============================================================= + + // Mask of an entry in packed address data. + uint256 private constant _BITMASK_ADDRESS_DATA_ENTRY = (1 << 64) - 1; + + // The bit position of `numberMinted` in packed address data. + uint256 private constant _BITPOS_NUMBER_MINTED = 64; + + // The bit position of `numberBurned` in packed address data. + uint256 private constant _BITPOS_NUMBER_BURNED = 128; + + // The bit position of `aux` in packed address data. + uint256 private constant _BITPOS_AUX = 192; + + // Mask of all 256 bits in packed address data except the 64 bits for `aux`. + uint256 private constant _BITMASK_AUX_COMPLEMENT = (1 << 192) - 1; + + // The bit position of `startTimestamp` in packed ownership. + uint256 private constant _BITPOS_START_TIMESTAMP = 160; + + // The bit mask of the `burned` bit in packed ownership. + uint256 private constant _BITMASK_BURNED = 1 << 224; + + // The bit position of the `nextInitialized` bit in packed ownership. + uint256 private constant _BITPOS_NEXT_INITIALIZED = 225; + + // The bit mask of the `nextInitialized` bit in packed ownership. + uint256 private constant _BITMASK_NEXT_INITIALIZED = 1 << 225; + + // The bit position of `extraData` in packed ownership. + uint256 private constant _BITPOS_EXTRA_DATA = 232; + + // Mask of all 256 bits in a packed ownership except the 24 bits for `extraData`. + uint256 private constant _BITMASK_EXTRA_DATA_COMPLEMENT = (1 << 232) - 1; + + // The mask of the lower 160 bits for addresses. + uint256 private constant _BITMASK_ADDRESS = (1 << 160) - 1; + + // The maximum `quantity` that can be minted with {_mintERC2309}. + // This limit is to prevent overflows on the address data entries. + // For a limit of 5000, a total of 3.689e15 calls to {_mintERC2309} + // is required to cause an overflow, which is unrealistic. + uint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000; + + // The `Transfer` event signature is given by: + // `keccak256(bytes("Transfer(address,address,uint256)"))`. + bytes32 private constant _TRANSFER_EVENT_SIGNATURE = + 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; + + // ============================================================= + // STORAGE + // ============================================================= + + // The next token ID to be minted. + uint256 private _currentIndex; + + // The number of tokens burned. + uint256 private _burnCounter; + + // Token name + string private _name; + + // Token symbol + string private _symbol; + + // Mapping from token ID to ownership details + // An empty struct value does not necessarily mean the token is unowned. + // See {_packedOwnershipOf} implementation for details. + // + // Bits Layout: + // - [0..159] `addr` + // - [160..223] `startTimestamp` + // - [224] `burned` + // - [225] `nextInitialized` + // - [232..255] `extraData` + mapping(uint256 => uint256) private _packedOwnerships; + + // Mapping owner address to address data. + // + // Bits Layout: + // - [0..63] `balance` + // - [64..127] `numberMinted` + // - [128..191] `numberBurned` + // - [192..255] `aux` + mapping(address => uint256) private _packedAddressData; + + // Mapping from token ID to approved address. + mapping(uint256 => TokenApprovalRef) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // The amount of tokens minted above `_sequentialUpTo()`. + // We call these spot mints (i.e. non-sequential mints). + uint256 private _spotMinted; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + _currentIndex = _startTokenId(); + + if (_sequentialUpTo() < _startTokenId()) _revert(SequentialUpToTooSmall.selector); + } + + // ============================================================= + // TOKEN COUNTING OPERATIONS + // ============================================================= + + /** + * @dev Returns the starting token ID for sequential mints. + * + * Override this function to change the starting token ID for sequential mints. + * + * Note: The value returned must never change after any tokens have been minted. + */ + function _startTokenId() internal view virtual returns (uint256) { + return 0; + } + + /** + * @dev Returns the maximum token ID (inclusive) for sequential mints. + * + * Override this function to return a value less than 2**256 - 1, + * but greater than `_startTokenId()`, to enable spot (non-sequential) mints. + * + * Note: The value returned must never change after any tokens have been minted. + */ + function _sequentialUpTo() internal view virtual returns (uint256) { + return type(uint256).max; + } + + /** + * @dev Returns the next token ID to be minted. + */ + function _nextTokenId() internal view virtual returns (uint256) { + return _currentIndex; + } + + /** + * @dev Returns the total number of tokens in existence. + * Burned tokens will reduce the count. + * To get the total number of tokens minted, please see {_totalMinted}. + */ + function totalSupply() public view virtual override returns (uint256 result) { + // Counter underflow is impossible as `_burnCounter` cannot be incremented + // more than `_currentIndex + _spotMinted - _startTokenId()` times. + unchecked { + // With spot minting, the intermediate `result` can be temporarily negative, + // and the computation must be unchecked. + result = _currentIndex - _burnCounter - _startTokenId(); + if (_sequentialUpTo() != type(uint256).max) result += _spotMinted; + } + } + + /** + * @dev Returns the total amount of tokens minted in the contract. + */ + function _totalMinted() internal view virtual returns (uint256 result) { + // Counter underflow is impossible as `_currentIndex` does not decrement, + // and it is initialized to `_startTokenId()`. + unchecked { + result = _currentIndex - _startTokenId(); + if (_sequentialUpTo() != type(uint256).max) result += _spotMinted; + } + } + + /** + * @dev Returns the total number of tokens burned. + */ + function _totalBurned() internal view virtual returns (uint256) { + return _burnCounter; + } + + /** + * @dev Returns the total number of tokens that are spot-minted. + */ + function _totalSpotMinted() internal view virtual returns (uint256) { + return _spotMinted; + } + + // ============================================================= + // ADDRESS DATA OPERATIONS + // ============================================================= + + /** + * @dev Returns the number of tokens in `owner`'s account. + */ + function balanceOf(address owner) public view virtual override returns (uint256) { + if (owner == address(0)) _revert(BalanceQueryForZeroAddress.selector); + return _packedAddressData[owner] & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the number of tokens minted by `owner`. + */ + function _numberMinted(address owner) internal view returns (uint256) { + return (_packedAddressData[owner] >> _BITPOS_NUMBER_MINTED) & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the number of tokens burned by or on behalf of `owner`. + */ + function _numberBurned(address owner) internal view returns (uint256) { + return (_packedAddressData[owner] >> _BITPOS_NUMBER_BURNED) & _BITMASK_ADDRESS_DATA_ENTRY; + } + + /** + * Returns the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). + */ + function _getAux(address owner) internal view returns (uint64) { + return uint64(_packedAddressData[owner] >> _BITPOS_AUX); + } + + /** + * Sets the auxiliary data for `owner`. (e.g. number of whitelist mint slots used). + * If there are multiple variables, please pack them into a uint64. + */ + function _setAux(address owner, uint64 aux) internal virtual { + uint256 packed = _packedAddressData[owner]; + uint256 auxCasted; + // Cast `aux` with assembly to avoid redundant masking. + assembly { + auxCasted := aux + } + packed = (packed & _BITMASK_AUX_COMPLEMENT) | (auxCasted << _BITPOS_AUX); + _packedAddressData[owner] = packed; + } + + // ============================================================= + // IERC165 + // ============================================================= + + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) + * to learn more about how these ids are created. + * + * This function call must use less than 30000 gas. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + // The interface IDs are constants representing the first 4 bytes + // of the XOR of all function selectors in the interface. + // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) + // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) + return + interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. + interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. + interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. + } + + // ============================================================= + // IERC721Metadata + // ============================================================= + + /** + * @dev Returns the token collection name. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the token collection symbol. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ''; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, it can be overridden in child contracts. + */ + function _baseURI() internal view virtual returns (string memory) { + return ''; + } + + // ============================================================= + // OWNERSHIPS OPERATIONS + // ============================================================= + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) public view virtual override returns (address) { + return address(uint160(_packedOwnershipOf(tokenId))); + } + + /** + * @dev Gas spent here starts off proportional to the maximum mint batch size. + * It gradually moves to O(1) as tokens get transferred around over time. + */ + function _ownershipOf(uint256 tokenId) internal view virtual returns (TokenOwnership memory) { + return _unpackedOwnership(_packedOwnershipOf(tokenId)); + } + + /** + * @dev Returns the unpacked `TokenOwnership` struct at `index`. + */ + function _ownershipAt(uint256 index) internal view virtual returns (TokenOwnership memory) { + return _unpackedOwnership(_packedOwnerships[index]); + } + + /** + * @dev Returns whether the ownership slot at `index` is initialized. + * An uninitialized slot does not necessarily mean that the slot has no owner. + */ + function _ownershipIsInitialized(uint256 index) internal view virtual returns (bool) { + return _packedOwnerships[index] != 0; + } + + /** + * @dev Initializes the ownership slot minted at `index` for efficiency purposes. + */ + function _initializeOwnershipAt(uint256 index) internal virtual { + if (_packedOwnerships[index] == uint256(0)) { + _packedOwnerships[index] = _packedOwnershipOf(index); + } + } + + /** + * @dev Returns the packed ownership data of `tokenId`. + */ + function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) { + if (_startTokenId() <= tokenId) { + packed = _packedOwnerships[tokenId]; + + if (tokenId > _sequentialUpTo()) { + if (_packedOwnershipExists(packed)) return packed; + _revert(OwnerQueryForNonexistentToken.selector); + } + + // If the data at the starting slot does not exist, start the scan. + if (packed == uint256(0)) { + if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); + // Invariant: + // There will always be an initialized ownership slot + // (i.e. `ownership.addr != address(0) && ownership.burned == false`) + // before an unintialized ownership slot + // (i.e. `ownership.addr == address(0) && ownership.burned == false`) + // Hence, `tokenId` will not underflow. + // + // We can directly compare the packed value. + // If the address is zero, packed will be zero. + for (;;) { + unchecked { + packed = _packedOwnerships[--tokenId]; + } + if (packed == uint256(0)) continue; + if (packed & _BITMASK_BURNED == uint256(0)) return packed; + // Otherwise, the token is burned, and we must revert. + // This handles the case of batch burned tokens, where only the burned bit + // of the starting slot is set, and remaining slots are left uninitialized. + _revert(OwnerQueryForNonexistentToken.selector); + } + } + // Otherwise, the data exists and we can skip the scan. + // This is possible because we have already achieved the target condition. + // This saves 2143 gas on transfers of initialized tokens. + // If the token is not burned, return `packed`. Otherwise, revert. + if (packed & _BITMASK_BURNED == uint256(0)) return packed; + } + _revert(OwnerQueryForNonexistentToken.selector); + } + + /** + * @dev Returns the unpacked `TokenOwnership` struct from `packed`. + */ + function _unpackedOwnership(uint256 packed) private pure returns (TokenOwnership memory ownership) { + ownership.addr = address(uint160(packed)); + ownership.startTimestamp = uint64(packed >> _BITPOS_START_TIMESTAMP); + ownership.burned = packed & _BITMASK_BURNED != 0; + ownership.extraData = uint24(packed >> _BITPOS_EXTRA_DATA); + } + + /** + * @dev Packs ownership data into a single uint256. + */ + function _packOwnershipData(address owner, uint256 flags) private view returns (uint256 result) { + assembly { + // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. + owner := and(owner, _BITMASK_ADDRESS) + // `owner | (block.timestamp << _BITPOS_START_TIMESTAMP) | flags`. + result := or(owner, or(shl(_BITPOS_START_TIMESTAMP, timestamp()), flags)) + } + } + + /** + * @dev Returns the `nextInitialized` flag set if `quantity` equals 1. + */ + function _nextInitializedFlag(uint256 quantity) private pure returns (uint256 result) { + // For branchless setting of the `nextInitialized` flag. + assembly { + // `(quantity == 1) << _BITPOS_NEXT_INITIALIZED`. + result := shl(_BITPOS_NEXT_INITIALIZED, eq(quantity, 1)) + } + } + + // ============================================================= + // APPROVAL OPERATIONS + // ============================================================= + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. See {ERC721A-_approve}. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + */ + function approve(address to, uint256 tokenId) public payable virtual override { + _approve(to, tokenId, true); + } + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) public view virtual override returns (address) { + if (!_exists(tokenId)) _revert(ApprovalQueryForNonexistentToken.selector); + + return _tokenApprovals[tokenId].value; + } + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} + * for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + _operatorApprovals[_msgSenderERC721A()][operator] = approved; + emit ApprovalForAll(_msgSenderERC721A(), operator, approved); + } + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll}. + */ + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted. See {_mint}. + */ + function _exists(uint256 tokenId) internal view virtual returns (bool result) { + if (_startTokenId() <= tokenId) { + if (tokenId > _sequentialUpTo()) return _packedOwnershipExists(_packedOwnerships[tokenId]); + + if (tokenId < _currentIndex) { + uint256 packed; + while ((packed = _packedOwnerships[tokenId]) == uint256(0)) --tokenId; + result = packed & _BITMASK_BURNED == uint256(0); + } + } + } + + /** + * @dev Returns whether `packed` represents a token that exists. + */ + function _packedOwnershipExists(uint256 packed) private pure returns (bool result) { + assembly { + // The following is equivalent to `owner != address(0) && burned == false`. + // Symbolically tested. + result := gt(and(packed, _BITMASK_ADDRESS), and(packed, _BITMASK_BURNED)) + } + } + + /** + * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. + */ + function _isSenderApprovedOrOwner( + uint256 approvedAddressValue, + uint256 ownerMasked, + uint256 msgSenderMasked + ) private pure returns (bool result) { + assembly { + result := or(eq(msgSenderMasked, ownerMasked), eq(msgSenderMasked, approvedAddressValue)) + } + } + + /** + * @dev Returns the storage slot and value for the approved address of `tokenId` casted to a uint256. + */ + function _getApprovedSlotAndValue(uint256 tokenId) + private + view + returns (uint256 approvedAddressSlot, uint256 approvedAddressValue) + { + TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; + // The following is equivalent to `approvedAddressValue = uint160(_tokenApprovals[tokenId].value)`. + assembly { + approvedAddressSlot := tokenApproval.slot + approvedAddressValue := sload(approvedAddressSlot) + } + } + + // ============================================================= + // TRANSFER OPERATIONS + // ============================================================= + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token + * by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public payable virtual override { + uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); + uint256 fromMasked = uint160(from); + + if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector); + + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + + // The nested ifs save around 20+ gas over a compound boolean condition. + if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) + if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + + _beforeTokenTransfers(from, to, tokenId, 1); + + assembly { + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. + } + } + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. + unchecked { + // We can directly increment and decrement the balances. + --_packedAddressData[from]; // Updates: `balance -= 1`. + ++_packedAddressData[to]; // Updates: `balance += 1`. + + // Updates: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transfering. + // - `burned` to `false`. + // - `nextInitialized` to `true`. + _packedOwnerships[tokenId] = _packOwnershipData( + to, + _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) + ); + + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { + uint256 nextTokenId = tokenId + 1; + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[nextTokenId] == uint256(0)) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } + } + } + + // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint160(to); + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + fromMasked, // `from`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector); + + _afterTokenTransfers(from, to, tokenId, 1); + } + + /** + * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public payable virtual override { + safeTransferFrom(from, to, tokenId, ''); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token + * by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) public payable virtual override { + transferFrom(from, to, tokenId); + if (to.code.length != 0) + if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + } + + /** + * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds)`. + */ + function _batchTransferFrom( + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { + _batchTransferFrom(address(0), from, to, tokenIds); + } + + /** + * @dev Transfers `tokenIds` in batch from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenIds` tokens must be owned by `from`. + * - `tokenIds` must be strictly ascending. + * - If `by` is not `from`, it must be approved to move these tokens + * by either {approve} or {setApprovalForAll}. + * + * `by` is the address that to check token approval for. + * If token approval check is not needed, pass in `address(0)` for `by`. + * + * Emits a {Transfer} event for each transfer. + */ + function _batchTransferFrom( + address by, + address from, + address to, + uint256[] memory tokenIds + ) internal virtual { + uint256 byMasked = uint160(by); + uint256 fromMasked = uint160(from); + uint256 toMasked = uint160(to); + // Disallow transfer to zero address. + if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector); + // Whether `by` may transfer the tokens. + bool mayTransfer = _orERC721A(byMasked == uint256(0), byMasked == fromMasked) || isApprovedForAll(from, by); + + // Early return if `tokenIds` is empty. + if (tokenIds.length == uint256(0)) return; + // The next `tokenId` to be minted (i.e. `_nextTokenId()`). + uint256 end = _currentIndex; + // Pointer to start and end (exclusive) of `tokenIds`. + (uint256 ptr, uint256 ptrEnd) = _mdataERC721A(tokenIds); + + uint256 prevTokenId; + uint256 prevOwnershipPacked; + unchecked { + do { + uint256 tokenId = _mloadERC721A(ptr); + uint256 miniBatchStart = tokenId; + // Revert `tokenId` is out of bounds. + if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) + _revert(OwnerQueryForNonexistentToken.selector); + // Revert if `tokenIds` is not strictly ascending. + if (prevOwnershipPacked != 0) + if (tokenId <= prevTokenId) _revert(TokenIdsNotStrictlyAscending.selector); + // 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 = _packedOwnerships[j]) == uint256(0); ) --j; + // If the initialized slot is burned, revert. + if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + // Check that `tokenId` is owned by `from`. + if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector); + + do { + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + _beforeTokenTransfers(address(uint160(fromMasked)), address(uint160(toMasked)), tokenId, 1); + // Revert if the sender is not authorized to transfer the token. + if (!mayTransfer) + if (byMasked != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); + assembly { + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. + } + // Emit the `Transfer` event. + log4(0, 0, _TRANSFER_EVENT_SIGNATURE, fromMasked, toMasked, tokenId) + } + + if (_mloadERC721A(ptr += 0x20) != ++tokenId) break; + if (ptr == ptrEnd) break; + } while (_packedOwnerships[tokenId] == uint256(0)); + + // Updates tokenId: + // - `address` to the next owner. + // - `startTimestamp` to the timestamp of transferring. + // - `burned` to `false`. + // - `nextInitialized` to `false`, as it is optional. + _packedOwnerships[miniBatchStart] = _packOwnershipData( + address(uint160(toMasked)), + _nextExtraData(address(uint160(fromMasked)), address(uint160(toMasked)), prevOwnershipPacked) + ); + uint256 miniBatchLength = tokenId - miniBatchStart; + // Update the address data. + _packedAddressData[address(uint160(fromMasked))] -= miniBatchLength; + _packedAddressData[address(uint160(toMasked))] += miniBatchLength; + // Initialize the next slot if needed. + if (tokenId != end) + if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; + // Perform the after hook for the batch. + _afterTokenTransfers( + address(uint160(fromMasked)), + address(uint160(toMasked)), + miniBatchStart, + miniBatchLength + ); + // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. + prevTokenId = tokenId - 1; + } while (ptr != ptrEnd); + } + } + + /** + * @dev Safely transfers `tokenIds` in batch from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenIds` tokens must be owned by `from`. + * - If `by` is not `from`, it must be approved to move these tokens + * by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each transferred token. + * + * `by` is the address that to check token approval for. + * If token approval check is not needed, pass in `address(0)` for `by`. + * + * Emits a {Transfer} event for each transfer. + */ + function _safeBatchTransferFrom( + address by, + address from, + address to, + uint256[] memory tokenIds, + bytes memory _data + ) internal virtual { + _batchTransferFrom(by, from, to, tokenIds); + + unchecked { + if (to.code.length != 0) { + for ((uint256 ptr, uint256 ptrEnd) = _mdataERC721A(tokenIds); ptr != ptrEnd; ptr += 0x20) { + if (!_checkContractOnERC721Received(from, to, _mloadERC721A(ptr), _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + } + } + } + } + + /** + * @dev Hook that is called before a set of serially-ordered token IDs + * are about to be transferred. This includes minting. + * And also called before burning one token. + * + * `startTokenId` - the first token ID to be transferred. + * `quantity` - the amount to be transferred. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} + + /** + * @dev Hook that is called after a set of serially-ordered token IDs + * have been transferred. This includes minting. + * And also called after one token has been burned. + * + * `startTokenId` - the first token ID to be transferred. + * `quantity` - the amount to be transferred. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been + * transferred to `to`. + * - When `from` is zero, `tokenId` has been minted for `to`. + * - When `to` is zero, `tokenId` has been burned by `from`. + * - `from` and `to` are never both zero. + */ + function _afterTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} + + /** + * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. + * + * `from` - Previous owner of the given token ID. + * `to` - Target address that will receive the token. + * `tokenId` - Token ID to be transferred. + * `_data` - Optional data to send along with the call. + * + * Returns whether the call correctly returned the expected magic value. + */ + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { + try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns ( + bytes4 retval + ) { + return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == uint256(0)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + + // ============================================================= + // MINT OPERATIONS + // ============================================================= + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {Transfer} event for each mint. + */ + function _mint(address to, uint256 quantity) internal virtual { + uint256 startTokenId = _currentIndex; + if (quantity == uint256(0)) _revert(MintZeroQuantity.selector); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // `balance` and `numberMinted` have a maximum limit of 2**64. + // `tokenId` has a maximum limit of 2**256. + unchecked { + // Updates: + // - `address` to the owner. + // - `startTimestamp` to the timestamp of minting. + // - `burned` to `false`. + // - `nextInitialized` to `quantity == 1`. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) + ); + + // Updates: + // - `balance += quantity`. + // - `numberMinted += quantity`. + // + // We can directly add to the `balance` and `numberMinted`. + _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); + + // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint160(to); + + if (toMasked == uint256(0)) _revert(MintToZeroAddress.selector); + + uint256 end = startTokenId + quantity; + uint256 tokenId = startTokenId; + + if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); + + do { + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + 0, // `address(0)`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + // The `!=` check ensures that large values of `quantity` + // that overflows uint256 will make the loop run out of gas. + } while (++tokenId != end); + + _currentIndex = end; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * This function is intended for efficient minting only during contract creation. + * + * It emits only one {ConsecutiveTransfer} as defined in + * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), + * instead of a sequence of {Transfer} event(s). + * + * Calling this function outside of contract creation WILL make your contract + * non-compliant with the ERC721 standard. + * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 + * {ConsecutiveTransfer} event is only permissible during contract creation. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {ConsecutiveTransfer} event. + */ + function _mintERC2309(address to, uint256 quantity) internal virtual { + uint256 startTokenId = _currentIndex; + if (to == address(0)) _revert(MintToZeroAddress.selector); + if (quantity == uint256(0)) _revert(MintZeroQuantity.selector); + if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are unrealistic due to the above check for `quantity` to be below the limit. + unchecked { + // Updates: + // - `balance += quantity`. + // - `numberMinted += quantity`. + // + // We can directly add to the `balance` and `numberMinted`. + _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); + + // Updates: + // - `address` to the owner. + // - `startTimestamp` to the timestamp of minting. + // - `burned` to `false`. + // - `nextInitialized` to `quantity == 1`. + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) + ); + + if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); + + emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to); + + _currentIndex = startTokenId + quantity; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + + /** + * @dev Safely mints `quantity` tokens and transfers them to `to`. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement + * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. + * - `quantity` must be greater than 0. + * + * See {_mint}. + * + * Emits a {Transfer} event for each mint. + */ + function _safeMint( + address to, + uint256 quantity, + bytes memory _data + ) internal virtual { + _mint(to, quantity); + + unchecked { + if (to.code.length != 0) { + uint256 end = _currentIndex; + uint256 index = end - quantity; + do { + if (!_checkContractOnERC721Received(address(0), to, index++, _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + } while (index < end); + // This prevents reentrancy to `_safeMint`. + // It does not prevent reentrancy to `_safeMintSpot`. + if (_currentIndex != end) revert(); + } + } + } + + /** + * @dev Equivalent to `_safeMint(to, quantity, '')`. + */ + function _safeMint(address to, uint256 quantity) internal virtual { + _safeMint(to, quantity, ''); + } + + /** + * @dev Mints a single token at `tokenId`. + * + * Note: A spot-minted `tokenId` that has been burned can be re-minted again. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` must be greater than `_sequentialUpTo()`. + * - `tokenId` must not exist. + * + * Emits a {Transfer} event for each mint. + */ + function _mintSpot(address to, uint256 tokenId) internal virtual { + if (tokenId <= _sequentialUpTo()) _revert(SpotMintTokenIdTooSmall.selector); + uint256 prevOwnershipPacked = _packedOwnerships[tokenId]; + if (_packedOwnershipExists(prevOwnershipPacked)) _revert(TokenAlreadyExists.selector); + + _beforeTokenTransfers(address(0), to, tokenId, 1); + + // Overflows are incredibly unrealistic. + // The `numberMinted` for `to` is incremented by 1, and has a max limit of 2**64 - 1. + // `_spotMinted` is incremented by 1, and has a max limit of 2**256 - 1. + unchecked { + // Updates: + // - `address` to the owner. + // - `startTimestamp` to the timestamp of minting. + // - `burned` to `false`. + // - `nextInitialized` to `true` (as `quantity == 1`). + _packedOwnerships[tokenId] = _packOwnershipData( + to, + _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked) + ); + + // Updates: + // - `balance += 1`. + // - `numberMinted += 1`. + // + // We can directly add to the `balance` and `numberMinted`. + _packedAddressData[to] += (1 << _BITPOS_NUMBER_MINTED) | 1; + + // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint160(to); + + if (toMasked == uint256(0)) _revert(MintToZeroAddress.selector); + + assembly { + // Emit the `Transfer` event. + log4( + 0, // Start of data (0, since no data). + 0, // End of data (0, since no data). + _TRANSFER_EVENT_SIGNATURE, // Signature. + 0, // `address(0)`. + toMasked, // `to`. + tokenId // `tokenId`. + ) + } + + ++_spotMinted; + } + + _afterTokenTransfers(address(0), to, tokenId, 1); + } + + /** + * @dev Safely mints a single token at `tokenId`. + * + * Note: A spot-minted `tokenId` that has been burned can be re-minted again. + * + * Requirements: + * + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}. + * - `tokenId` must be greater than `_sequentialUpTo()`. + * - `tokenId` must not exist. + * + * See {_mintSpot}. + * + * Emits a {Transfer} event. + */ + function _safeMintSpot( + address to, + uint256 tokenId, + bytes memory _data + ) internal virtual { + _mintSpot(to, tokenId); + + unchecked { + if (to.code.length != 0) { + uint256 currentSpotMinted = _spotMinted; + if (!_checkContractOnERC721Received(address(0), to, tokenId, _data)) { + _revert(TransferToNonERC721ReceiverImplementer.selector); + } + // This prevents reentrancy to `_safeMintSpot`. + // It does not prevent reentrancy to `_safeMint`. + if (_spotMinted != currentSpotMinted) revert(); + } + } + } + + /** + * @dev Equivalent to `_safeMintSpot(to, tokenId, '')`. + */ + function _safeMintSpot(address to, uint256 tokenId) internal virtual { + _safeMintSpot(to, tokenId, ''); + } + + // ============================================================= + // APPROVAL OPERATIONS + // ============================================================= + + /** + * @dev Equivalent to `_approve(to, tokenId, false)`. + */ + function _approve(address to, uint256 tokenId) internal virtual { + _approve(to, tokenId, false); + } + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the + * zero address clears previous approvals. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function _approve( + address to, + uint256 tokenId, + bool approvalCheck + ) internal virtual { + address owner = ownerOf(tokenId); + + if (approvalCheck && _msgSenderERC721A() != owner) + if (!isApprovedForAll(owner, _msgSenderERC721A())) { + _revert(ApprovalCallerNotOwnerNorApproved.selector); + } + + _tokenApprovals[tokenId].value = to; + emit Approval(owner, to, tokenId); + } + + // ============================================================= + // BURN OPERATIONS + // ============================================================= + + /** + * @dev Equivalent to `_burn(tokenId, false)`. + */ + function _burn(uint256 tokenId) internal virtual { + _burn(tokenId, false); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + function _burn(uint256 tokenId, bool approvalCheck) internal virtual { + uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); + + uint256 fromMasked = uint160(prevOwnershipPacked); + address from = address(uint160(fromMasked)); + + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + + if (approvalCheck) { + // The nested ifs save around 20+ gas over a compound boolean condition. + if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) + if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + } + + _beforeTokenTransfers(from, address(0), tokenId, 1); + + assembly { + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. + } + } + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + // Counter overflow is incredibly unrealistic as `tokenId` would have to be 2**256. + unchecked { + // Updates: + // - `balance -= 1`. + // - `numberBurned += 1`. + // + // We can directly decrement the balance, and increment the number burned. + // This is equivalent to `packed -= 1; packed += 1 << _BITPOS_NUMBER_BURNED;`. + _packedAddressData[from] += (1 << _BITPOS_NUMBER_BURNED) - 1; + + // Updates: + // - `address` to the last owner. + // - `startTimestamp` to the timestamp of burning. + // - `burned` to `true`. + // - `nextInitialized` to `true`. + _packedOwnerships[tokenId] = _packOwnershipData( + from, + (_BITMASK_BURNED | _BITMASK_NEXT_INITIALIZED) | _nextExtraData(from, address(0), prevOwnershipPacked) + ); + + // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { + uint256 nextTokenId = tokenId + 1; + // If the next slot's address is zero and not burned (i.e. packed value is zero). + if (_packedOwnerships[nextTokenId] == uint256(0)) { + // If the next slot is within bounds. + if (nextTokenId != _currentIndex) { + // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. + _packedOwnerships[nextTokenId] = prevOwnershipPacked; + } + } + } + } + + emit Transfer(from, address(0), tokenId); + _afterTokenTransfers(from, address(0), tokenId, 1); + + // Overflow not possible, as `_burnCounter` cannot be exceed `_currentIndex + _spotMinted` times. + unchecked { + _burnCounter++; + } + } + + /** + * @dev Destroys `tokenIds`. + * Approvals are not cleared when tokenIds are burned. + * + * Requirements: + * + * - `tokenIds` must exist. + * - `tokenIds` must be strictly ascending. + * - `by` must be approved to burn these tokens by either {approve} or {setApprovalForAll}. + * + * `by` is the address that to check token approval for. + * If token approval check is not needed, pass in `address(0)` for `by`. + * + * Emits a {Transfer} event for each token burned. + */ + function _batchBurn(address by, uint256[] memory tokenIds) internal virtual { + // Early return if `tokenIds` is empty. + if (tokenIds.length == uint256(0)) return; + // The next `tokenId` to be minted (i.e. `_nextTokenId()`). + uint256 end = _currentIndex; + // Pointer to start and end (exclusive) of `tokenIds`. + (uint256 ptr, uint256 ptrEnd) = _mdataERC721A(tokenIds); + + uint256 prevOwnershipPacked; + address prevTokenOwner; + uint256 prevTokenId; + bool mayBurn; + unchecked { + do { + uint256 tokenId = _mloadERC721A(ptr); + uint256 miniBatchStart = tokenId; + // Revert `tokenId` is out of bounds. + if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) + _revert(OwnerQueryForNonexistentToken.selector); + // Revert if `tokenIds` is not strictly ascending. + if (prevOwnershipPacked != 0) + if (tokenId <= prevTokenId) _revert(TokenIdsNotStrictlyAscending.selector); + // 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 = _packedOwnerships[j]) == uint256(0); ) --j; + // If the initialized slot is burned, revert. + if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + + address tokenOwner = address(uint160(prevOwnershipPacked)); + if (tokenOwner != prevTokenOwner) { + prevTokenOwner = tokenOwner; + mayBurn = _orERC721A(by == address(0), tokenOwner == by) || isApprovedForAll(tokenOwner, by); + } + + do { + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + _beforeTokenTransfers(tokenOwner, address(0), tokenId, 1); + // Revert if the sender is not authorized to transfer the token. + if (!mayBurn) + if (uint160(by) != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); + assembly { + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. + } + // Emit the `Transfer` event. + log4(0, 0, _TRANSFER_EVENT_SIGNATURE, and(_BITMASK_ADDRESS, tokenOwner), 0, tokenId) + } + if (_mloadERC721A(ptr += 0x20) != ++tokenId) break; + if (ptr == ptrEnd) break; + } while (_packedOwnerships[tokenId] == uint256(0)); + + // Updates tokenId: + // - `address` to the same `tokenOwner`. + // - `startTimestamp` to the timestamp of transferring. + // - `burned` to `true`. + // - `nextInitialized` to `false`, as it is optional. + _packedOwnerships[miniBatchStart] = _packOwnershipData( + tokenOwner, + _BITMASK_BURNED | _nextExtraData(tokenOwner, address(0), prevOwnershipPacked) + ); + uint256 miniBatchLength = tokenId - miniBatchStart; + // Update the address data. + _packedAddressData[tokenOwner] += (miniBatchLength << _BITPOS_NUMBER_BURNED) - miniBatchLength; + // Initialize the next slot if needed. + if (tokenId != end) + if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; + // Perform the after hook for the batch. + _afterTokenTransfers(tokenOwner, address(0), miniBatchStart, miniBatchLength); + // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. + prevTokenId = tokenId - 1; + } while (ptr != ptrEnd); + // Increment the overall burn counter. + _burnCounter += tokenIds.length; + } + } + + // ============================================================= + // EXTRA DATA OPERATIONS + // ============================================================= + + /** + * @dev Directly sets the extra data for the ownership data `index`. + */ + function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual { + uint256 packed = _packedOwnerships[index]; + if (packed == uint256(0)) _revert(OwnershipNotInitializedForExtraData.selector); + uint256 extraDataCasted; + // Cast `extraData` with assembly to avoid redundant masking. + assembly { + extraDataCasted := extraData + } + packed = (packed & _BITMASK_EXTRA_DATA_COMPLEMENT) | (extraDataCasted << _BITPOS_EXTRA_DATA); + _packedOwnerships[index] = packed; + } + + /** + * @dev Called during each token transfer to set the 24bit `extraData` field. + * Intended to be overridden by the cosumer contract. + * + * `previousExtraData` - the value of `extraData` before transfer. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, `tokenId` will be burned by `from`. + * - `from` and `to` are never both zero. + */ + function _extraData( + address from, + address to, + uint24 previousExtraData + ) internal view virtual returns (uint24) {} + + /** + * @dev Returns the next extra data for the packed ownership data. + * The returned result is shifted into position. + */ + function _nextExtraData( + address from, + address to, + uint256 prevOwnershipPacked + ) private view returns (uint256) { + uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); + return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; + } + + // ============================================================= + // PRIVATE HELPERS + // ============================================================= + + /** + * @dev Returns a memory pointer to the start of `a`'s data. + */ + function _mdataERC721A(uint256[] memory a) private pure returns (uint256 start, uint256 end) { + assembly { + start := add(a, 0x20) + end := add(start, shl(5, mload(a))) + } + } + + /** + * @dev Returns the uint256 at `p` in memory. + */ + function _mloadERC721A(uint256 p) private pure returns (uint256 result) { + assembly { + result := mload(p) + } + } + + /** + * @dev Branchless boolean or. + */ + function _orERC721A(bool a, bool b) private pure returns (bool result) { + assembly { + result := or(iszero(iszero(a)), iszero(iszero(b))) + } + } + + // ============================================================= + // OTHER OPERATIONS + // ============================================================= + + /** + * @dev Returns the message sender (defaults to `msg.sender`). + * + * If you are writing GSN compatible contracts, you need to override this function. + */ + function _msgSenderERC721A() internal view virtual returns (address) { + return msg.sender; + } + + /** + * @dev Converts a uint256 to its ASCII string decimal representation. + */ + function _toString(uint256 value) internal pure virtual returns (string memory str) { + assembly { + // The maximum value of a uint256 contains 78 digits (1 byte per digit), but + // we allocate 0xa0 bytes to keep the free memory pointer 32-byte word aligned. + // We will need 1 word for the trailing zeros padding, 1 word for the length, + // and 3 words for a maximum of 78 digits. Total: 5 * 0x20 = 0xa0. + let m := add(mload(0x40), 0xa0) + // Update the free memory pointer to allocate. + mstore(0x40, m) + // Assign the `str` to the end. + str := sub(m, 0x20) + // Zeroize the slot after the string. + mstore(str, 0) + + // Cache the end of the memory to calculate the length later. + let end := str + + // We write the string from rightmost digit to leftmost digit. + // The following is essentially a do-while loop that also handles the zero case. + // prettier-ignore + for { let temp := value } 1 {} { + str := sub(str, 1) + // Write the character to the pointer. + // The ASCII index of the '0' character is 48. + mstore8(str, add(48, mod(temp, 10))) + // Keep dividing `temp` until zero. + temp := div(temp, 10) + // prettier-ignore + if iszero(temp) { break } + } + + let length := sub(end, str) + // Move the pointer 32 bytes leftwards to make room for the length. + str := sub(str, 0x20) + // Store the length. + mstore(str, length) + } + } + + /** + * @dev For more efficient reverts. + */ + function _revert(bytes4 errorSelector) internal pure { + assembly { + mstore(0x00, errorSelector) + revert(0x00, 0x04) + } + } +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol new file mode 100644 index 0000000..029232c --- /dev/null +++ b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; +import {ERC721A} from "erc721a/contracts/ERC721A.sol"; +import {IERC721A} from "erc721a/contracts/IERC721A.sol"; + +/** + * @title ERC721AConduitPreapprovedCloneable + * @notice ERC721A with the MagicEden conduit preapproved. + */ +abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneable { + /// @dev The canonical MagicEden conduit. + address internal constant _CONDUIT = + 0x2052f8A2Ff46283B30084e5d84c89A2fdBE7f74b; + + /** + * @dev Returns if the `operator` is allowed to manage all of the + * assets of `owner`. Always returns true for the conduit. + */ + function isApprovedForAll(address owner, address operator) + public + view + virtual + override (ERC721A, IERC721A) + returns (bool) + { + if (operator == _CONDUIT) { + return true; + } + return ERC721A.isApprovedForAll(owner, operator); + } +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol new file mode 100644 index 0000000..4881af8 --- /dev/null +++ b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.3.0 +// Creator: Chiru Labs + +pragma solidity ^0.8.4; + +import {IERC721AQueryable} from "erc721a/contracts/extensions/IERC721AQueryable.sol"; +import {ERC721ACloneable} from "./ERC721ACloneable.sol"; + +/** + * @title ERC721AQueryableCloneable. + * + * @dev ERC721A subclass with convenience query functions. + */ +abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryable { + /** + * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. + * + * If the `tokenId` is out of bounds: + * + * - `addr = address(0)` + * - `startTimestamp = 0` + * - `burned = false` + * - `extraData = 0` + * + * If the `tokenId` is burned: + * + * - `addr =
` + * - `startTimestamp = ` + * - `burned = true` + * - `extraData = ` + * + * Otherwise: + * + * - `addr =
` + * - `startTimestamp = ` + * - `burned = false` + * - `extraData = ` + */ + function explicitOwnershipOf(uint256 tokenId) + public + view + virtual + override + returns (TokenOwnership memory ownership) + { + unchecked { + if (tokenId >= _startTokenId()) { + if (tokenId > _sequentialUpTo()) return _ownershipAt(tokenId); + + if (tokenId < _nextTokenId()) { + // If the `tokenId` is within bounds, + // scan backwards for the initialized ownership slot. + while (!_ownershipIsInitialized(tokenId)) --tokenId; + return _ownershipAt(tokenId); + } + } + } + } + + /** + * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. + * See {ERC721AQueryable-explicitOwnershipOf} + */ + function explicitOwnershipsOf(uint256[] calldata tokenIds) + external + view + virtual + override + returns (TokenOwnership[] memory) + { + TokenOwnership[] memory ownerships; + uint256 i = tokenIds.length; + assembly { + // Grab the free memory pointer. + ownerships := mload(0x40) + // Store the length. + mstore(ownerships, i) + // Allocate one word for the length, + // `tokenIds.length` words for the pointers. + i := shl(5, i) // Multiply `i` by 32. + mstore(0x40, add(add(ownerships, 0x20), i)) + } + while (i != 0) { + uint256 tokenId; + assembly { + i := sub(i, 0x20) + tokenId := calldataload(add(tokenIds.offset, i)) + } + TokenOwnership memory ownership = explicitOwnershipOf(tokenId); + assembly { + // Store the pointer of `ownership` in the `ownerships` array. + mstore(add(add(ownerships, 0x20), i), ownership) + } + } + return ownerships; + } + + /** + * @dev Returns an array of token IDs owned by `owner`, + * in the range [`start`, `stop`) + * (i.e. `start <= tokenId < stop`). + * + * This function allows for tokens to be queried if the collection + * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. + * + * Requirements: + * + * - `start < stop` + */ + function tokensOfOwnerIn( + address owner, + uint256 start, + uint256 stop + ) external view virtual override returns (uint256[] memory) { + return _tokensOfOwnerIn(owner, start, stop); + } + + /** + * @dev Returns an array of token IDs owned by `owner`. + * + * This function scans the ownership mapping and is O(`totalSupply`) in complexity. + * It is meant to be called off-chain. + * + * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into + * multiple smaller scans if the collection is large enough to cause + * an out-of-gas error (10K collections should be fine). + */ + function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { + // If spot mints are enabled, full-range scan is disabled. + if (_sequentialUpTo() != type(uint256).max) _revert(NotCompatibleWithSpotMints.selector); + uint256 start = _startTokenId(); + uint256 stop = _nextTokenId(); + uint256[] memory tokenIds; + if (start != stop) tokenIds = _tokensOfOwnerIn(owner, start, stop); + return tokenIds; + } + + /** + * @dev Helper function for returning an array of token IDs owned by `owner`. + * + * Note that this function is optimized for smaller bytecode size over runtime gas, + * since it is meant to be called off-chain. + */ + function _tokensOfOwnerIn( + address owner, + uint256 start, + uint256 stop + ) private view returns (uint256[] memory tokenIds) { + unchecked { + if (start >= stop) _revert(InvalidQueryRange.selector); + // Set `start = max(start, _startTokenId())`. + if (start < _startTokenId()) start = _startTokenId(); + uint256 nextTokenId = _nextTokenId(); + // If spot mints are enabled, scan all the way until the specified `stop`. + uint256 stopLimit = _sequentialUpTo() != type(uint256).max ? stop : nextTokenId; + // Set `stop = min(stop, stopLimit)`. + if (stop >= stopLimit) stop = stopLimit; + // Number of tokens to scan. + uint256 tokenIdsMaxLength = balanceOf(owner); + // Set `tokenIdsMaxLength` to zero if the range contains no tokens. + if (start >= stop) tokenIdsMaxLength = 0; + // If there are one or more tokens to scan. + if (tokenIdsMaxLength != 0) { + // Set `tokenIdsMaxLength = min(balanceOf(owner), tokenIdsMaxLength)`. + if (stop - start <= tokenIdsMaxLength) tokenIdsMaxLength = stop - start; + uint256 m; // Start of available memory. + assembly { + // Grab the free memory pointer. + tokenIds := mload(0x40) + // Allocate one word for the length, and `tokenIdsMaxLength` words + // for the data. `shl(5, x)` is equivalent to `mul(32, x)`. + m := add(tokenIds, shl(5, add(tokenIdsMaxLength, 1))) + mstore(0x40, m) + } + // We need to call `explicitOwnershipOf(start)`, + // because the slot at `start` may not be initialized. + TokenOwnership memory ownership = explicitOwnershipOf(start); + address currOwnershipAddr; + // If the starting slot exists (i.e. not burned), + // initialize `currOwnershipAddr`. + // `ownership.address` will not be zero, + // as `start` is clamped to the valid token ID range. + if (!ownership.burned) currOwnershipAddr = ownership.addr; + uint256 tokenIdsIdx; + // Use a do-while, which is slightly more efficient for this case, + // as the array will at least contain one element. + do { + if (_sequentialUpTo() != type(uint256).max) { + // Skip the remaining unused sequential slots. + if (start == nextTokenId) start = _sequentialUpTo() + 1; + // Reset `currOwnershipAddr`, as each spot-minted token is a batch of one. + if (start > _sequentialUpTo()) currOwnershipAddr = address(0); + } + ownership = _ownershipAt(start); // This implicitly allocates memory. + assembly { + switch mload(add(ownership, 0x40)) + // if `ownership.burned == false`. + case 0 { + // if `ownership.addr != address(0)`. + // The `addr` already has it's upper 96 bits clearned, + // since it is written to memory with regular Solidity. + if mload(ownership) { + currOwnershipAddr := mload(ownership) + } + // if `currOwnershipAddr == owner`. + // The `shl(96, x)` is to make the comparison agnostic to any + // dirty upper 96 bits in `owner`. + if iszero(shl(96, xor(currOwnershipAddr, owner))) { + tokenIdsIdx := add(tokenIdsIdx, 1) + mstore(add(tokenIds, shl(5, tokenIdsIdx)), start) + } + } + // Otherwise, reset `currOwnershipAddr`. + // This handles the case of batch burned tokens + // (burned bit of first slot set, remaining slots left uninitialized). + default { + currOwnershipAddr := 0 + } + start := add(start, 1) + // Free temporary memory implicitly allocated for ownership + // to avoid quadratic memory expansion costs. + mstore(0x40, m) + } + } while (!(start == stop || tokenIdsIdx == tokenIdsMaxLength)); + // Store the length of the array. + assembly { + mstore(tokenIds, tokenIdsIdx) + } + } + } + } +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol new file mode 100644 index 0000000..967cd2c --- /dev/null +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {ERC2981} from "solady/src/tokens/ERC2981.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; + +import {ERC721AConduitPreapprovedCloneable} from "./ERC721AConduitPreapprovedCloneable.sol"; +import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; +import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; + +contract ERC721MagicDropMetadata is ERC721AConduitPreapprovedCloneable, IERC721MagicDropMetadata, ERC2981, Ownable { + /*============================================================== + = STORAGE = + ==============================================================*/ + + /// @notice The base URI for the token metadata + string private _tokenBaseURI; + + /// @notice The contract URI for contract metadata + string private _contractURI; + + /// @notice The max supply of tokens to be minted + uint256 private _maxSupply; + + /// @notice The max number of tokens a wallet can mint + uint256 private _walletLimit; + + /// @notice The provenance hash for guarenteeing metadata integrity + bytes32 private _provenanceHash; + + /// @notice The royalty receiver for the collection + address private _royaltyReceiver; + + /// @notice The royalty basis points for the collection + uint96 private _royaltyBps; + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Returns the base URI for the token metadata, overriding the ERC721A + /// @return The base URI for the token metadata + function baseURI() public view override returns (string memory) { + return _baseURI(); + } + + /// @notice Returns the contract URI for contract metadata + /// @return The contract URI for contract metadata + function contractURI() public view override returns (string memory) { + return _contractURI; + } + + /// @notice Returns the max supply of tokens to be minted + /// @return The max supply of tokens to be minted + function maxSupply() public view returns (uint256) { + return _maxSupply; + } + + /// @notice Returns the max number of tokens a wallet can mint + /// @return The max number of tokens a wallet can mint + function walletLimit() public view returns (uint256) { + return _walletLimit; + } + + /// @notice Returns the provenance hash for guarenteeing metadata integrity + /// @return The provenance hash for guarenteeing metadata integrity + function provenanceHash() public view returns (bytes32) { + return _provenanceHash; + } + + /// @notice Returns the royalty address for the collection + /// @return The royalty address for the collection + function royaltyAddress() public view returns (address) { + return _royaltyReceiver; + } + + /// @notice Returns the royalty basis points for the collection + /// @return The royalty basis points for the collection + function royaltyBps() public view returns (uint256) { + return _royaltyBps; + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets the base URI for the token URIs + /// @param baseURI The base URI to set + function setBaseURI(string calldata baseURI) external override onlyOwner { + _tokenBaseURI = baseURI; + + if (totalSupply() != 0) { + emit BatchMetadataUpdate(0, totalSupply() - 1); + } + } + + /// @notice Sets the contract URI for contract metadata + /// @param contractURI The contract URI to set + function setContractURI(string calldata contractURI) external override onlyOwner { + _contractURI = contractURI; + + emit ContractURIUpdated(contractURI); + } + + /// @notice Sets the max supply of tokens to be minted + /// @param newMaxSupply The max supply of tokens to be minted + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + // Ensure the new max supply is not greater than the current max supply + if (newMaxSupply > _maxSupply) { + revert MaxSupplyCannotBeIncreased(); + } + + // Ensure the new max supply is greater than the current supply + if (newMaxSupply < totalSupply()) { + revert MaxSupplyCannotBeLessThanCurrentSupply(); + } + + _maxSupply = newMaxSupply; + + emit MaxSupplyUpdated(newMaxSupply); + } + + /// @notice Sets the max number of tokens a wallet can mint + /// @param newWalletLimit The max number of tokens a wallet can mint + function setWalletLimit(uint256 newWalletLimit) external onlyOwner { + _walletLimit = newWalletLimit; + + emit WalletLimitUpdated(newWalletLimit); + } + + /// @notice Sets the provenance hash for guarenteeing metadata integrity + /// for random reveals. Created using a hash of the metadata. + /// Reverts if the provenance hash is updated after any tokens have been minted. + /// @param newProvenanceHash The provenance hash to set + function setProvenanceHash(bytes32 newProvenanceHash) external onlyOwner { + if (_totalMinted() > 0) { + revert ProvenanceHashCannotBeUpdated(); + } + + bytes32 oldProvenanceHash = _provenanceHash; + _provenanceHash = newProvenanceHash; + + emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); + } + + /// @notice Sets the royalty info for the contract + /// @param newReceiver The address to receive royalties + /// @param newBps The royalty basis points (100 = 1%) + function setRoyaltyInfo(address newReceiver, uint96 newBps) external onlyOwner { + _royaltyReceiver = newReceiver; + _royaltyBps = newBps; + + super._setDefaultRoyalty(_royaltyReceiver, _royaltyBps); + + emit RoyaltyInfoUpdated(_royaltyReceiver, _royaltyBps); + } + + /// @notice Emit an event notifying metadata updates for a range of token ids (EIP-4906) + /// @param fromTokenId The start token id. + /// @param toTokenId The end token id. + function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external onlyOwner { + emit BatchMetadataUpdate(fromTokenId, toTokenId); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Returns the base URI for the token metadata, overriding the ERC721A + /// @return The base URI for the token metadata + function _baseURI() internal view override returns (string memory) { + return _tokenBaseURI; + } +} diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol new file mode 100644 index 0000000..9bc287f --- /dev/null +++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; + +interface IERC721MagicDropMetadata is IERC2981 { + /*============================================================== + = EVENTS = + ==============================================================*/ + + /// @notice Emitted when the contract URI is updated. + /// @param _contractURI The new contract URI. + event ContractURIUpdated(string _contractURI); + + /// @notice Emitted when the max supply is updated. + /// @param _maxSupply The new max supply. + event MaxSupplyUpdated(uint256 _maxSupply); + + /// @notice Emitted when the wallet limit is updated. + /// @param _walletLimit The new wallet limit. + event WalletLimitUpdated(uint256 _walletLimit); + + /// @notice Emitted when the provenance hash is updated. + /// @param oldHash The old provenance hash. + /// @param newHash The new provenance hash. + event ProvenanceHashUpdated(bytes32 oldHash, bytes32 newHash); + + /// @notice Emitted when the royalty info is updated. + /// @param receiver The new royalty receiver. + /// @param bps The new royalty basis points. + event RoyaltyInfoUpdated(address receiver, uint256 bps); + + /// @notice Emitted when the metadata is updated. (EIP-4906) + /// @param _fromTokenId The starting token ID. + /// @param _toTokenId The ending token ID. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + /*============================================================== + = ERRORS = + ==============================================================*/ + + /// @notice Throw when the provenance hash cannot be updated. + error ProvenanceHashCannotBeUpdated(); + + /// @notice Throw when the max supply is exceeded. + error CannotExceedMaxSupply(); + + /// @notice Throw when the max supply is less than the current supply. + error MaxSupplyCannotBeLessThanCurrentSupply(); + + /// @notice Throw when trying to increase the max supply. + error MaxSupplyCannotBeIncreased(); + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Returns the base URI used to construct token URIs + /// @dev This is concatenated with the token ID to form the complete token URI + /// @return The base URI string that prefixes all token URIs + function baseURI() external view returns (string memory); + + /// @notice Returns the contract-level metadata URI + /// @dev Used by marketplaces like MagicEden to display collection information + /// @return The URI string pointing to the contract's metadata JSON + function contractURI() external view returns (string memory); + + /// @notice Returns the maximum number of tokens that can be minted + /// @dev This value cannot be increased once set, only decreased + /// @return The maximum supply cap for the collection + function maxSupply() external view returns (uint256); + + /// @notice Returns the maximum number of tokens that can be minted per wallet + /// @dev Used to prevent excessive concentration of tokens in single wallets + /// @return The maximum number of tokens allowed per wallet address + function walletLimit() external view returns (uint256); + + /// @notice Returns the provenance hash for the collection + /// @dev Used to prove that the token metadata/artwork hasn't been changed after mint + /// @return The 32-byte provenance hash of the collection + function provenanceHash() external view returns (bytes32); + + /// @notice Returns the address that receives royalty payments + /// @dev Used in conjunction with royaltyBps for EIP-2981 royalty standard + /// @return The address designated to receive royalty payments + function royaltyAddress() external view returns (address); + + /// @notice Returns the royalty percentage in basis points (1/100th of a percent) + /// @dev 100 basis points = 1%. Used in EIP-2981 royalty calculations + /// @return The royalty percentage in basis points (e.g., 250 = 2.5%) + function royaltyBps() external view returns (uint256); + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets the base URI for all token metadata + /// @dev This is a critical function that determines where all token metadata is hosted + /// Changing this will update the metadata location for all tokens in the collection + /// @param baseURI The new base URI string that will prefix all token URIs + function setBaseURI(string calldata baseURI) external; + + /// @notice Sets the contract-level metadata URI + /// @dev This metadata is used by marketplaces to display collection information + /// Should point to a JSON file following collection metadata standards + /// @param contractURI The new URI string pointing to the contract's metadata JSON + function setContractURI(string calldata contractURI) external; + + /// @notice Updates the maximum supply cap for the collection + /// @dev Can only decrease the max supply, never increase it + /// Must be greater than or equal to the current total supply + /// @param maxSupply The new maximum number of tokens that can be minted + function setMaxSupply(uint256 maxSupply) external; + + /// @notice Updates the per-wallet token holding limit + /// @dev Used to prevent token concentration and ensure fair distribution + /// Setting this to 0 effectively removes the wallet limit + /// @param walletLimit The new maximum number of tokens allowed per wallet + function setWalletLimit(uint256 walletLimit) external; + + /// @notice Sets the provenance hash for the collection + /// @dev Should only be called once before the collection is revealed + /// Used to verify the integrity of the artwork/metadata after reveal + /// @param provenanceHash The 32-byte hash representing the collection's provenance + function setProvenanceHash(bytes32 provenanceHash) external; + + /// @notice Updates the royalty configuration for the collection + /// @dev Implements EIP-2981 for NFT royalty standards + /// The bps (basis points) must be between 0 and 10000 (0% to 100%) + /// @param newReceiver The address that will receive future royalty payments + /// @param newBps The royalty percentage in basis points (e.g., 250 = 2.5%) + function setRoyaltyInfo(address newReceiver, uint96 newBps) external; +} diff --git a/test/erc721m/ERC721MInitializableTest.t.sol b/test/erc721m/ERC721MInitializableTest.t.sol new file mode 100644 index 0000000..b13fffb --- /dev/null +++ b/test/erc721m/ERC721MInitializableTest.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {console} from "forge-std/console.sol"; +import {LibClone} from "solady/src/utils/LibClone.sol"; +import {IERC721A} from "erc721a/contracts/IERC721A.sol"; +import {Test} from "forge-std/Test.sol"; +import {ERC721MInitializableV1_0_1 as ERC721MInitializable} from + "../../contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol"; +import {MintStageInfo} from "../../contracts/common/Structs.sol"; +import {ErrorsAndEvents} from "../../contracts/common/ErrorsAndEvents.sol"; + +contract MockERC721M is ERC721MInitializable { + function baseURI() public view returns (string memory) { + return _currentBaseURI; + } + + function tokenURISuffix() public view returns (string memory) { + return _tokenURISuffix; + } +} + +contract ERC721MInitializableTest is Test { + MockERC721M public nft; + address public owner; + address public minter; + address public fundReceiver; + address public readonly; + uint256 public constant INITIAL_SUPPLY = 1000; + uint256 public constant GLOBAL_WALLET_LIMIT = 0; + + function setUp() public { + owner = address(this); + fundReceiver = address(0x1); + readonly = address(0x2); + minter = address(0x4); + + vm.deal(owner, 10 ether); + vm.deal(minter, 2 ether); + + address clone = LibClone.deployERC1967(address(new MockERC721M())); + nft = MockERC721M(clone); + nft.initialize("Test", "TEST", owner); + nft.setup( + "base_uri_", + ".json", + INITIAL_SUPPLY, + GLOBAL_WALLET_LIMIT, + address(0), + fundReceiver, + new MintStageInfo[](0), + address(this), + 0 + ); + } + + function testTransferWhenNotFrozen() public { + vm.startPrank(owner); + nft.setFrozen(false); + nft.ownerMint(1, minter); + vm.stopPrank(); + + vm.prank(minter); + nft.transferFrom(minter, readonly, 0); + + assertEq(nft.balanceOf(minter), 0); + assertEq(nft.balanceOf(readonly), 1); + } + + function testTransferWhenFrozen() public { + vm.startPrank(owner); + nft.setFrozen(true); + nft.ownerMint(1, minter); + vm.stopPrank(); + + vm.expectRevert(ERC721MInitializable.TransfersAreFrozen.selector); + vm.prank(minter); + nft.safeTransferFrom(minter, readonly, 0); + } + + function testBaseURISetup() public { + assertEq(nft.baseURI(), "base_uri_"); + } + + function testBaseURISuffixSetup() public { + assertEq(nft.tokenURISuffix(), ".json"); + } + + function testSetBaseURI() public { + vm.startPrank(owner); + nft.setBaseURI("new_base_uri_"); + assertEq(nft.baseURI(), "new_base_uri_"); + } + + function testSetTokenURISuffix() public { + vm.startPrank(owner); + nft.setTokenURISuffix(".txt"); + assertEq(nft.tokenURISuffix(), ".txt"); + } +} From 6473f8f6da4b194a00a906050c9e390b61e314ac Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 17:14:15 -0500 Subject: [PATCH 02/26] lint Signed-off-by: Adam Wolf --- .../nft/erc721m/clones/ERC721ACloneable.sol | 232 +++++++----------- .../ERC721AConduitPreapprovedCloneable.sol | 7 +- .../clones/ERC721AQueryableCloneable.sol | 32 ++- .../ERC721MagicDropMetadataCloneable.sol | 2 +- lib/ERC721A | 1 - 5 files changed, 105 insertions(+), 169 deletions(-) delete mode 160000 lib/ERC721A diff --git a/contracts/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol index aaabe91..53e4e18 100644 --- a/contracts/nft/erc721m/clones/ERC721ACloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721ACloneable.sol @@ -12,12 +12,9 @@ import {Initializable} from "solady/src/utils/Initializable.sol"; * @dev Interface of ERC721 token receiver. */ interface ERC721A__IERC721Receiver { - function onERC721Received( - address operator, - address from, - uint256 tokenId, - bytes calldata data - ) external returns (bytes4); + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + returns (bytes4); } /** @@ -295,10 +292,9 @@ contract ERC721ACloneable is IERC721A, Initializable { // of the XOR of all function selectors in the interface. // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) - return - interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. - interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. - interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. + return interfaceId == 0x01ffc9a7 // ERC165 interface ID for ERC165. + || interfaceId == 0x80ac58cd // ERC165 interface ID for ERC721. + || interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. } // ============================================================= @@ -326,7 +322,7 @@ contract ERC721ACloneable is IERC721A, Initializable { if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector); string memory baseURI = _baseURI(); - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ''; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ""; } /** @@ -335,7 +331,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * by default, it can be overridden in child contracts. */ function _baseURI() internal view virtual returns (string memory) { - return ''; + return ""; } // ============================================================= @@ -549,11 +545,11 @@ contract ERC721ACloneable is IERC721A, Initializable { /** * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. */ - function _isSenderApprovedOrOwner( - uint256 approvedAddressValue, - uint256 ownerMasked, - uint256 msgSenderMasked - ) private pure returns (bool result) { + function _isSenderApprovedOrOwner(uint256 approvedAddressValue, uint256 ownerMasked, uint256 msgSenderMasked) + private + pure + returns (bool result) + { assembly { result := or(eq(msgSenderMasked, ownerMasked), eq(msgSenderMasked, approvedAddressValue)) } @@ -592,11 +588,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) public payable virtual override { + function transferFrom(address from, address to, uint256 tokenId) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); uint256 fromMasked = uint160(from); @@ -605,15 +597,14 @@ contract ERC721ACloneable is IERC721A, Initializable { (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) + if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) { if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + } _beforeTokenTransfers(from, to, tokenId, 1); assembly { - if approvedAddressValue { - sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. - } + if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. } // Underflow of the sender's balance is impossible because we check for @@ -629,10 +620,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. // - `nextInitialized` to `true`. - _packedOwnerships[tokenId] = _packOwnershipData( - to, - _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) - ); + _packedOwnerships[tokenId] = + _packOwnershipData(to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { @@ -669,12 +658,8 @@ contract ERC721ACloneable is IERC721A, Initializable { /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public payable virtual override { - safeTransferFrom(from, to, tokenId, ''); + function safeTransferFrom(address from, address to, uint256 tokenId) public payable virtual override { + safeTransferFrom(from, to, tokenId, ""); } /** @@ -692,27 +677,24 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes memory _data - ) public payable virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) + public + payable + virtual + override + { transferFrom(from, to, tokenId); - if (to.code.length != 0) + if (to.code.length != 0) { if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { _revert(TransferToNonERC721ReceiverImplementer.selector); } + } } /** * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds)`. */ - function _batchTransferFrom( - address from, - address to, - uint256[] memory tokenIds - ) internal virtual { + function _batchTransferFrom(address from, address to, uint256[] memory tokenIds) internal virtual { _batchTransferFrom(address(0), from, to, tokenIds); } @@ -733,12 +715,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event for each transfer. */ - function _batchTransferFrom( - address by, - address from, - address to, - uint256[] memory tokenIds - ) internal virtual { + function _batchTransferFrom(address by, address from, address to, uint256[] memory tokenIds) internal virtual { uint256 byMasked = uint160(by); uint256 fromMasked = uint160(from); uint256 toMasked = uint160(to); @@ -761,15 +738,19 @@ contract ERC721ACloneable is IERC721A, Initializable { uint256 tokenId = _mloadERC721A(ptr); uint256 miniBatchStart = tokenId; // Revert `tokenId` is out of bounds. - if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) + if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) { _revert(OwnerQueryForNonexistentToken.selector); + } // Revert if `tokenIds` is not strictly ascending. - if (prevOwnershipPacked != 0) + if (prevOwnershipPacked != 0) { if (tokenId <= prevTokenId) _revert(TokenIdsNotStrictlyAscending.selector); + } // 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 = _packedOwnerships[j]) == uint256(0); ) --j; + for (uint256 j = tokenId; (prevOwnershipPacked = _packedOwnerships[j]) == uint256(0);) { + --j; + } // If the initialized slot is burned, revert. if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); // Check that `tokenId` is owned by `from`. @@ -779,12 +760,11 @@ contract ERC721ACloneable is IERC721A, Initializable { (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); _beforeTokenTransfers(address(uint160(fromMasked)), address(uint160(toMasked)), tokenId, 1); // Revert if the sender is not authorized to transfer the token. - if (!mayTransfer) + if (!mayTransfer) { if (byMasked != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); + } assembly { - if approvedAddressValue { - sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. - } + if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. // Emit the `Transfer` event. log4(0, 0, _TRANSFER_EVENT_SIGNATURE, fromMasked, toMasked, tokenId) } @@ -807,14 +787,12 @@ contract ERC721ACloneable is IERC721A, Initializable { _packedAddressData[address(uint160(fromMasked))] -= miniBatchLength; _packedAddressData[address(uint160(toMasked))] += miniBatchLength; // Initialize the next slot if needed. - if (tokenId != end) + if (tokenId != end) { if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; + } // Perform the after hook for the batch. _afterTokenTransfers( - address(uint160(fromMasked)), - address(uint160(toMasked)), - miniBatchStart, - miniBatchLength + address(uint160(fromMasked)), address(uint160(toMasked)), miniBatchStart, miniBatchLength ); // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. prevTokenId = tokenId - 1; @@ -840,13 +818,10 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event for each transfer. */ - function _safeBatchTransferFrom( - address by, - address from, - address to, - uint256[] memory tokenIds, - bytes memory _data - ) internal virtual { + function _safeBatchTransferFrom(address by, address from, address to, uint256[] memory tokenIds, bytes memory _data) + internal + virtual + { _batchTransferFrom(by, from, to, tokenIds); unchecked { @@ -876,12 +851,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs @@ -899,12 +869,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ - function _afterTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. @@ -916,12 +881,10 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Returns whether the call correctly returned the expected magic value. */ - function _checkContractOnERC721Received( - address from, - address to, - uint256 tokenId, - bytes memory _data - ) private returns (bool) { + function _checkContractOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) + private + returns (bool) + { try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns ( bytes4 retval ) { @@ -965,10 +928,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) - ); + _packedOwnerships[startTokenId] = + _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); // Updates: // - `balance += quantity`. @@ -1051,10 +1012,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) - ); + _packedOwnerships[startTokenId] = + _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); @@ -1078,11 +1037,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event for each mint. */ - function _safeMint( - address to, - uint256 quantity, - bytes memory _data - ) internal virtual { + function _safeMint(address to, uint256 quantity, bytes memory _data) internal virtual { _mint(to, quantity); unchecked { @@ -1105,7 +1060,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Equivalent to `_safeMint(to, quantity, '')`. */ function _safeMint(address to, uint256 quantity) internal virtual { - _safeMint(to, quantity, ''); + _safeMint(to, quantity, ""); } /** @@ -1137,10 +1092,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `true` (as `quantity == 1`). - _packedOwnerships[tokenId] = _packOwnershipData( - to, - _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked) - ); + _packedOwnerships[tokenId] = + _packOwnershipData(to, _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked)); // Updates: // - `balance += 1`. @@ -1187,11 +1140,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function _safeMintSpot( - address to, - uint256 tokenId, - bytes memory _data - ) internal virtual { + function _safeMintSpot(address to, uint256 tokenId, bytes memory _data) internal virtual { _mintSpot(to, tokenId); unchecked { @@ -1211,7 +1160,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Equivalent to `_safeMintSpot(to, tokenId, '')`. */ function _safeMintSpot(address to, uint256 tokenId) internal virtual { - _safeMintSpot(to, tokenId, ''); + _safeMintSpot(to, tokenId, ""); } // ============================================================= @@ -1238,17 +1187,14 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits an {Approval} event. */ - function _approve( - address to, - uint256 tokenId, - bool approvalCheck - ) internal virtual { + function _approve(address to, uint256 tokenId, bool approvalCheck) internal virtual { address owner = ownerOf(tokenId); - if (approvalCheck && _msgSenderERC721A() != owner) + if (approvalCheck && _msgSenderERC721A() != owner) { if (!isApprovedForAll(owner, _msgSenderERC721A())) { _revert(ApprovalCallerNotOwnerNorApproved.selector); } + } _tokenApprovals[tokenId].value = to; emit Approval(owner, to, tokenId); @@ -1285,16 +1231,15 @@ contract ERC721ACloneable is IERC721A, Initializable { if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) + if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) { if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + } } _beforeTokenTransfers(from, address(0), tokenId, 1); assembly { - if approvedAddressValue { - sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. - } + if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. } // Underflow of the sender's balance is impossible because we check for @@ -1374,15 +1319,19 @@ contract ERC721ACloneable is IERC721A, Initializable { uint256 tokenId = _mloadERC721A(ptr); uint256 miniBatchStart = tokenId; // Revert `tokenId` is out of bounds. - if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) + if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) { _revert(OwnerQueryForNonexistentToken.selector); + } // Revert if `tokenIds` is not strictly ascending. - if (prevOwnershipPacked != 0) + if (prevOwnershipPacked != 0) { if (tokenId <= prevTokenId) _revert(TokenIdsNotStrictlyAscending.selector); + } // 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 = _packedOwnerships[j]) == uint256(0); ) --j; + for (uint256 j = tokenId; (prevOwnershipPacked = _packedOwnerships[j]) == uint256(0);) { + --j; + } // If the initialized slot is burned, revert. if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); @@ -1396,12 +1345,11 @@ contract ERC721ACloneable is IERC721A, Initializable { (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); _beforeTokenTransfers(tokenOwner, address(0), tokenId, 1); // Revert if the sender is not authorized to transfer the token. - if (!mayBurn) + if (!mayBurn) { if (uint160(by) != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); + } assembly { - if approvedAddressValue { - sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. - } + if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. // Emit the `Transfer` event. log4(0, 0, _TRANSFER_EVENT_SIGNATURE, and(_BITMASK_ADDRESS, tokenOwner), 0, tokenId) } @@ -1415,15 +1363,15 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `burned` to `true`. // - `nextInitialized` to `false`, as it is optional. _packedOwnerships[miniBatchStart] = _packOwnershipData( - tokenOwner, - _BITMASK_BURNED | _nextExtraData(tokenOwner, address(0), prevOwnershipPacked) + tokenOwner, _BITMASK_BURNED | _nextExtraData(tokenOwner, address(0), prevOwnershipPacked) ); uint256 miniBatchLength = tokenId - miniBatchStart; // Update the address data. _packedAddressData[tokenOwner] += (miniBatchLength << _BITPOS_NUMBER_BURNED) - miniBatchLength; // Initialize the next slot if needed. - if (tokenId != end) + if (tokenId != end) { if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; + } // Perform the after hook for the batch. _afterTokenTransfers(tokenOwner, address(0), miniBatchStart, miniBatchLength); // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. @@ -1467,21 +1415,13 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _extraData( - address from, - address to, - uint24 previousExtraData - ) internal view virtual returns (uint24) {} + function _extraData(address from, address to, uint24 previousExtraData) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ - function _nextExtraData( - address from, - address to, - uint256 prevOwnershipPacked - ) private view returns (uint256) { + function _nextExtraData(address from, address to, uint256 prevOwnershipPacked) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } @@ -1582,4 +1522,4 @@ contract ERC721ACloneable is IERC721A, Initializable { revert(0x00, 0x04) } } -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol index 029232c..2873626 100644 --- a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol @@ -11,8 +11,7 @@ import {IERC721A} from "erc721a/contracts/IERC721A.sol"; */ abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneable { /// @dev The canonical MagicEden conduit. - address internal constant _CONDUIT = - 0x2052f8A2Ff46283B30084e5d84c89A2fdBE7f74b; + address internal constant _CONDUIT = 0x2052f8A2Ff46283B30084e5d84c89A2fdBE7f74b; /** * @dev Returns if the `operator` is allowed to manage all of the @@ -22,7 +21,7 @@ abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneabl public view virtual - override (ERC721A, IERC721A) + override(ERC721A, IERC721A) returns (bool) { if (operator == _CONDUIT) { @@ -30,4 +29,4 @@ abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneabl } return ERC721A.isApprovedForAll(owner, operator); } -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol index 4881af8..82a0f2b 100644 --- a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol @@ -108,11 +108,13 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab * * - `start < stop` */ - function tokensOfOwnerIn( - address owner, - uint256 start, - uint256 stop - ) external view virtual override returns (uint256[] memory) { + function tokensOfOwnerIn(address owner, uint256 start, uint256 stop) + external + view + virtual + override + returns (uint256[] memory) + { return _tokensOfOwnerIn(owner, start, stop); } @@ -142,11 +144,11 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab * Note that this function is optimized for smaller bytecode size over runtime gas, * since it is meant to be called off-chain. */ - function _tokensOfOwnerIn( - address owner, - uint256 start, - uint256 stop - ) private view returns (uint256[] memory tokenIds) { + function _tokensOfOwnerIn(address owner, uint256 start, uint256 stop) + private + view + returns (uint256[] memory tokenIds) + { unchecked { if (start >= stop) _revert(InvalidQueryRange.selector); // Set `start = max(start, _startTokenId())`. @@ -200,9 +202,7 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab // if `ownership.addr != address(0)`. // The `addr` already has it's upper 96 bits clearned, // since it is written to memory with regular Solidity. - if mload(ownership) { - currOwnershipAddr := mload(ownership) - } + if mload(ownership) { currOwnershipAddr := mload(ownership) } // if `currOwnershipAddr == owner`. // The `shl(96, x)` is to make the comparison agnostic to any // dirty upper 96 bits in `owner`. @@ -214,9 +214,7 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab // Otherwise, reset `currOwnershipAddr`. // This handles the case of batch burned tokens // (burned bit of first slot set, remaining slots left uninitialized). - default { - currOwnershipAddr := 0 - } + default { currOwnershipAddr := 0 } start := add(start, 1) // Free temporary memory implicitly allocated for ownership // to avoid quadratic memory expansion costs. @@ -230,4 +228,4 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab } } } -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 967cd2c..8907a7a 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -88,7 +88,7 @@ contract ERC721MagicDropMetadata is ERC721AConduitPreapprovedCloneable, IERC721M /// @param baseURI The base URI to set function setBaseURI(string calldata baseURI) external override onlyOwner { _tokenBaseURI = baseURI; - + if (totalSupply() != 0) { emit BatchMetadataUpdate(0, totalSupply() - 1); } diff --git a/lib/ERC721A b/lib/ERC721A deleted file mode 160000 index 6f8a82a..0000000 --- a/lib/ERC721A +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6f8a82a7b2833ad8b2fc7b54349281143a731fdd From 4a672f0a0097abbc27b9bdcccafc79583dfe99f2 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 17:14:20 -0500 Subject: [PATCH 03/26] forge install: ERC721A v4.3.0 --- lib/ERC721A | 1 + 1 file changed, 1 insertion(+) create mode 160000 lib/ERC721A diff --git a/lib/ERC721A b/lib/ERC721A new file mode 160000 index 0000000..6f8a82a --- /dev/null +++ b/lib/ERC721A @@ -0,0 +1 @@ +Subproject commit 6f8a82a7b2833ad8b2fc7b54349281143a731fdd From 159ec2ff061c61fe5bd13b26b126bb82582de474 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 17:18:15 -0500 Subject: [PATCH 04/26] add cloneables Signed-off-by: Adam Wolf --- cli/cmds/utils | 10 + .../erc721m/ERC721CMInitializableV1_0_0.sol | 539 ------------------ .../erc721m/ERC721MInitializableV1_0_0.sol | 539 ------------------ .../nft/erc721m/clones/ERC721ACloneable.sol | 515 +++++------------ test/erc721m/ERC721CMInitializableTest.t.sol | 38 +- 5 files changed, 195 insertions(+), 1446 deletions(-) delete mode 100644 contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol delete mode 100644 contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol diff --git a/cli/cmds/utils b/cli/cmds/utils index 2ec2ced..5d40d93 100644 --- a/cli/cmds/utils +++ b/cli/cmds/utils @@ -101,6 +101,16 @@ save_deployment_data() { } prestart() { + # Check Node.js version + NODE_VERSION=$(node -v) + REQUIRED_VERSION="v20.9.0" + if [[ "$(printf '%s\n' "$REQUIRED_VERSION" "$NODE_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]]; then + echo "Node.js version is $NODE_VERSION. Please upgrade to version 20.9 or greater." + echo "You can use nvm to install and switch versions: https://github.com/nvm-sh/nvm" + echo "nvm install 20.9 or nvm use 20.9" + exit 1 + fi + # Fetch the latest changes from the remote git -C "$BASE_DIR" fetch diff --git a/contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol b/contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol deleted file mode 100644 index 5681d64..0000000 --- a/contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol +++ /dev/null @@ -1,539 +0,0 @@ -//SPDX-License-Identifier: MIT - -pragma solidity ^0.8.22; - -import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; -import {ERC2981} from "solady/src/tokens/ERC2981.sol"; -import {Ownable} from "solady/src/auth/Ownable.sol"; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; -import { - ERC721ACQueryableInitializable, - ERC721AUpgradeable, - IERC721AUpgradeable -} from "../creator-token-standards/ERC721ACQueryableInitializable.sol"; -import {ERC721MStorage} from "./ERC721MStorage.sol"; -import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; -import {MintStageInfo} from "../../common/Structs.sol"; -import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; -import {Cosignable} from "../../common/Cosignable.sol"; -import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; - -/** - * @title ERC721CMInitializableV1_0_0 - * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this - * contract is for use with EIP-1167 Minimal Proxies (Clones). - */ -contract ERC721CMInitializableV1_0_0 is - IERC721MInitializable, - ERC721ACQueryableInitializable, - ERC2981, - Ownable, - ReentrancyGuard, - Cosignable, - AuthorizedMinterControl, - ERC721MStorage -{ - /*============================================================== - = INITIALIZERS = - ==============================================================*/ - - constructor() { - _disableInitializers(); - } - - /// @notice Initializes the contract - /// @param name The name of the token collection - /// @param symbol The symbol of the token collection - /// @param initialOwner The address of the initial owner - function initialize(string calldata name, string calldata symbol, address initialOwner) - external - initializer - initializerERC721A - { - if (initialOwner == address(0)) { - revert InitialOwnerCannotBeZero(); - } - - __ERC721ACQueryableInitializable_init(name, symbol); - _initializeOwner(initialOwner); - } - - /*============================================================== - = META = - ==============================================================*/ - - /// @notice Returns the contract name and version - /// @return The contract name and version as strings - function contractNameAndVersion() public pure returns (string memory, string memory) { - return ("ERC721CMInitializable", "1.0.0"); - } - - /// @notice Gets the token URI for a specific token ID - /// @param tokenId The ID of the token - /// @return The token URI - function tokenURI(uint256 tokenId) - public - view - override(ERC721AUpgradeable, IERC721AUpgradeable) - returns (string memory) - { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _currentBaseURI; - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; - } - - /// @notice Gets the contract URI - /// @return The contract URI - function contractURI() public view returns (string memory) { - return _contractURI; - } - - /*============================================================== - = MODIFIERS = - ==============================================================*/ - - /// @notice Modifier to check if the contract is mintable - modifier canMint() { - if (!_mintable) revert NotMintable(); - _; - } - - /// @notice Modifier to check if the total supply is enough - /// @param qty The quantity to mint - modifier hasSupply(uint256 qty) { - if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); - _; - } - - /*============================================================== - = PUBLIC WRITE METHODS = - ==============================================================*/ - - /// @notice Mints tokens for the caller - /// @param qty The quantity to mint - /// @param limit The minting limit for the caller (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) - external - payable - virtual - nonReentrant - { - _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); - } - - /// @notice Allows authorized minters to mint tokens for a specified address - /// @param to The address to mint tokens for - /// @param qty The quantity to mint - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function authorizedMint( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) external payable onlyAuthorizedMinter { - _mintInternal(qty, to, limit, proof, timestamp, signature); - } - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @notice Gets the stage info for a given stage index - /// @param index The stage index - /// @return The stage info, wallet minted count, and stage minted count - function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { - if (index >= _mintStages.length) { - revert InvalidStage(); - } - uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; - uint256 stageMinted = _stageMintedCounts[index]; - return (_mintStages[index], walletMinted, stageMinted); - } - - /// @notice Gets the mint currency address - /// @return The address of the mint currency - function getMintCurrency() external view returns (address) { - return _mintCurrency; - } - - /// @notice Gets the cosign nonce for a specific minter - /// @param minter The address of the minter - /// @return The cosign nonce - function getCosignNonce(address minter) public view returns (uint256) { - return _numberMinted(minter); - } - - /// @notice Gets the mintable status - /// @return The mintable status - function getMintable() external view returns (bool) { - return _mintable; - } - - /// @notice Gets the number of minting stages - /// @return The number of minting stages - function getNumberStages() external view override returns (uint256) { - return _mintStages.length; - } - - /// @notice Gets the maximum mintable supply - /// @return The maximum mintable supply - function getMaxMintableSupply() external view override returns (uint256) { - return _maxMintableSupply; - } - - /// @notice Gets the global wallet limit - /// @return The global wallet limit - function getGlobalWalletLimit() external view override returns (uint256) { - return _globalWalletLimit; - } - - /// @notice Gets the total minted count for a specific address - /// @param a The address to get the minted count for - /// @return The total minted count - function totalMintedByAddress(address a) external view virtual override returns (uint256) { - return _numberMinted(a); - } - - /// @notice Gets the active stage from the timestamp - /// @param timestamp The timestamp to get the active stage from - /// @return The active stage - function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { - for (uint256 i = 0; i < _mintStages.length; i++) { - if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { - return i; - } - } - revert InvalidStage(); - } - - /// @notice Checks if the contract supports a given interface - /// @param interfaceId The interface identifier - /// @return True if the contract supports the interface, false otherwise - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC2981, IERC721AUpgradeable, ERC721ACQueryableInitializable) - returns (bool) - { - return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) - || ERC721ACQueryableInitializable.supportsInterface(interfaceId); - } - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @notice Sets up the contract with initial parameters - /// @param maxMintableSupply The maximum mintable supply - /// @param globalWalletLimit The global wallet limit - /// @param mintCurrency The address of the mint currency - /// @param fundReceiver The address to receive funds - /// @param initialStages The initial mint stages - /// @param royaltyReceiver The address to receive royalties - /// @param royaltyFeeNumerator The royalty fee numerator - function setup( - uint256 maxMintableSupply, - uint256 globalWalletLimit, - address mintCurrency, - address fundReceiver, - MintStageInfo[] calldata initialStages, - address royaltyReceiver, - uint96 royaltyFeeNumerator - ) external onlyOwner { - if (globalWalletLimit > maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _mintable = true; - _maxMintableSupply = maxMintableSupply; - _globalWalletLimit = globalWalletLimit; - _mintCurrency = mintCurrency; - _fundReceiver = fundReceiver; - _setTimestampExpirySeconds(300); // 5 minutes - - if (initialStages.length > 0) { - _setStages(initialStages); - } - - if (royaltyReceiver != address(0)) { - setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); - } - } - - /// @notice Adds an authorized minter - /// @param minter The address to add as an authorized minter - function addAuthorizedMinter(address minter) external override onlyOwner { - _addAuthorizedMinter(minter); - } - - /// @notice Removes an authorized minter - /// @param minter The address to remove as an authorized minter - function removeAuthorizedMinter(address minter) external override onlyOwner { - _removeAuthorizedMinter(minter); - } - - /// @notice Sets the cosigner address - /// @param cosigner The address to set as the cosigner - function setCosigner(address cosigner) external override onlyOwner { - _setCosigner(cosigner); - } - - /// @notice Sets the timestamp expiry seconds - /// @param timestampExpirySeconds The expiry time in seconds for timestamps - function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { - _setTimestampExpirySeconds(timestampExpirySeconds); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function setStages(MintStageInfo[] calldata newStages) external onlyOwner { - _setStages(newStages); - } - - /// @notice Sets the mintable status - /// @param mintable The mintable status to set - function setMintable(bool mintable) external onlyOwner { - _mintable = mintable; - emit SetMintable(mintable); - } - - /// @notice Sets the default royalty for the contract - /// @param receiver The address to receive royalties - /// @param feeNumerator The royalty fee numerator - function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { - super._setDefaultRoyalty(receiver, feeNumerator); - emit DefaultRoyaltySet(receiver, feeNumerator); - } - - /// @notice Sets the maximum mintable supply - /// @param maxMintableSupply The maximum mintable supply to set - function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { - if (maxMintableSupply > _maxMintableSupply) { - revert CannotIncreaseMaxMintableSupply(); - } - _maxMintableSupply = maxMintableSupply; - emit SetMaxMintableSupply(maxMintableSupply); - } - - /// @notice Sets the global wallet limit - /// @param globalWalletLimit The global wallet limit to set - function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { - if (globalWalletLimit > _maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _globalWalletLimit = globalWalletLimit; - emit SetGlobalWalletLimit(globalWalletLimit); - } - - /// @notice Allows the owner to mint tokens for a specific address - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { - _safeMint(to, qty); - } - - /// @notice Withdraws the total mint fee and remaining balance from the contract - /// @dev Can only be called by the owner - function withdraw() external onlyOwner { - (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); - if (!success) revert TransferFailed(); - _totalMintFee = 0; - - uint256 remainingValue = address(this).balance; - (success,) = _fundReceiver.call{value: remainingValue}(""); - if (!success) revert WithdrawFailed(); - - emit Withdraw(_totalMintFee + remainingValue); - } - - /// @notice Withdraws ERC20 tokens from the contract - /// @dev Can only be called by the owner - function withdrawERC20() external onlyOwner { - if (_mintCurrency == address(0)) revert WrongMintCurrency(); - - uint256 totalFee = _totalMintFee; - uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); - - if (remaining < totalFee) revert InsufficientBalance(); - - _totalMintFee = 0; - uint256 totalAmount = totalFee + remaining; - - SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); - SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); - - emit WithdrawERC20(_mintCurrency, totalAmount); - } - - /// @notice Sets the base URI for the token URIs - /// @param baseURI The base URI to set - function setBaseURI(string calldata baseURI) external onlyOwner { - _currentBaseURI = baseURI; - emit SetBaseURI(baseURI); - } - - /// @notice Sets the token URI suffix - /// @param suffix The suffix to set - function setTokenURISuffix(string calldata suffix) external onlyOwner { - _tokenURISuffix = suffix; - } - - /// @notice Sets the contract URI - /// @param uri The URI to set - function setContractURI(string calldata uri) external onlyOwner { - _contractURI = uri; - emit SetContractURI(uri); - } - - /*============================================================== - = INTERNAL HELPERS = - ==============================================================*/ - - /// @notice Internal function to handle minting logic - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function _mintInternal( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) internal canMint hasSupply(qty) { - uint256 stageTimestamp = block.timestamp; - bool waiveMintFee = false; - - if (getCosigner() != address(0)) { - waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); - _assertValidTimestamp(timestamp); - stageTimestamp = timestamp; - } - - uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); - MintStageInfo memory stage = _mintStages[activeStage]; - - uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; - - // Check value if minting with ETH - if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); - - // Check stage supply if applicable - if (stage.maxStageSupply > 0) { - if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { - revert StageSupplyExceeded(); - } - } - - // Check global wallet limit if applicable - if (_globalWalletLimit > 0) { - if (_numberMinted(to) + qty > _globalWalletLimit) { - revert WalletGlobalLimitExceeded(); - } - } - - // Check wallet limit for stage if applicable, limit == 0 means no limit enforced - if (stage.walletLimit > 0) { - if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { - revert WalletStageLimitExceeded(); - } - } - - // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required - if (stage.merkleRoot != 0) { - if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { - revert InvalidProof(); - } - - // Verify merkle proof mint limit - if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { - revert WalletStageLimitExceeded(); - } - } - - if (_mintCurrency != address(0)) { - // ERC20 mint payment - SafeTransferLib.safeTransferFrom( - _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty - ); - } - - _totalMintFee += adjustedMintFee * qty; - - _stageMintedCountsPerWallet[activeStage][to] += qty; - _stageMintedCounts[activeStage] += qty; - _safeMint(to, qty); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function _setStages(MintStageInfo[] calldata newStages) internal { - delete _mintStages; - - for (uint256 i = 0; i < newStages.length;) { - if (i >= 1) { - if ( - newStages[i].startTimeUnixSeconds - < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() - ) { - revert InsufficientStageTimeGap(); - } - } - _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); - _mintStages.push( - MintStageInfo({ - price: newStages[i].price, - mintFee: newStages[i].mintFee, - walletLimit: newStages[i].walletLimit, - merkleRoot: newStages[i].merkleRoot, - maxStageSupply: newStages[i].maxStageSupply, - startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, - endTimeUnixSeconds: newStages[i].endTimeUnixSeconds - }) - ); - emit UpdateStage( - i, - newStages[i].price, - newStages[i].mintFee, - newStages[i].walletLimit, - newStages[i].merkleRoot, - newStages[i].maxStageSupply, - newStages[i].startTimeUnixSeconds, - newStages[i].endTimeUnixSeconds - ); - - unchecked { - ++i; - } - } - } - - /// @notice Validates the start and end timestamps for a stage - /// @param start The start timestamp - /// @param end The end timestamp - function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { - if (start >= end) revert InvalidStartAndEndTimestamp(); - } - - /// @notice Requires the caller to be the contract owner - function _requireCallerIsContractOwner() internal view override { - return _checkOwner(); - } - - /// @dev Overriden to prevent double-initialization of the owner. - function _guardInitializeOwner() internal pure virtual override returns (bool) { - return true; - } -} diff --git a/contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol b/contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol deleted file mode 100644 index b394ba8..0000000 --- a/contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol +++ /dev/null @@ -1,539 +0,0 @@ -//SPDX-License-Identifier: MIT - -pragma solidity ^0.8.22; - -import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; -import {ERC2981} from "solady/src/tokens/ERC2981.sol"; -import {Ownable} from "solady/src/auth/Ownable.sol"; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; -import {Initializable} from "solady/src/utils/Initializable.sol"; - -import {ERC721AUpgradeable, IERC721AUpgradeable} from "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; -import { - ERC721AQueryableUpgradeable, - IERC721AQueryableUpgradeable -} from "erc721a-upgradeable/contracts/extensions/ERC721AQueryableUpgradeable.sol"; -import {IERC721A, ERC721A} from "erc721a/contracts/extensions/ERC721AQueryable.sol"; -import {ERC721MStorage} from "./ERC721MStorage.sol"; -import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; -import {MintStageInfo} from "../../common/Structs.sol"; -import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; -import {Cosignable} from "../../common/Cosignable.sol"; -import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; - -/** - * @title ERC721MInitializableV1_0_0 - * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this - * contract is for use with EIP-1167 Minimal Proxies (Clones). - */ -contract ERC721MInitializableV1_0_0 is - IERC721MInitializable, - ERC721AQueryableUpgradeable, - ERC2981, - Ownable, - ReentrancyGuard, - Cosignable, - AuthorizedMinterControl, - ERC721MStorage, - Initializable -{ - /*============================================================== - = INITIALIZERS = - ==============================================================*/ - - constructor() { - _disableInitializers(); - } - - /// @notice Initializes the contract - /// @param name The name of the token collection - /// @param symbol The symbol of the token collection - /// @param initialOwner The address of the initial owner - function initialize(string calldata name, string calldata symbol, address initialOwner) - external - initializer - initializerERC721A - { - if (initialOwner == address(0)) { - revert InitialOwnerCannotBeZero(); - } - - __ERC721A_init_unchained(name, symbol); - __ERC721AQueryable_init_unchained(); - _initializeOwner(initialOwner); - } - - /*============================================================== - = META = - ==============================================================*/ - - /// @notice Returns the contract name and version - /// @return The contract name and version as strings - function contractNameAndVersion() public pure returns (string memory, string memory) { - return ("ERC721CMInitializable", "1.0.0"); - } - - /// @notice Gets the token URI for a specific token ID - /// @param tokenId The ID of the token - /// @return The token URI - function tokenURI(uint256 tokenId) - public - view - override(ERC721AUpgradeable, IERC721AUpgradeable) - returns (string memory) - { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _currentBaseURI; - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; - } - - /// @notice Gets the contract URI - /// @return The contract URI - function contractURI() public view returns (string memory) { - return _contractURI; - } - - /*============================================================== - = MODIFIERS = - ==============================================================*/ - - /// @notice Modifier to check if the contract is mintable - modifier canMint() { - if (!_mintable) revert NotMintable(); - _; - } - - /// @notice Modifier to check if the total supply is enough - /// @param qty The quantity to mint - modifier hasSupply(uint256 qty) { - if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); - _; - } - - /*============================================================== - = PUBLIC WRITE METHODS = - ==============================================================*/ - - /// @notice Mints tokens for the caller - /// @param qty The quantity to mint - /// @param limit The minting limit for the caller (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) - external - payable - virtual - nonReentrant - { - _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); - } - - /// @notice Allows authorized minters to mint tokens for a specified address - /// @param to The address to mint tokens for - /// @param qty The quantity to mint - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function authorizedMint( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) external payable onlyAuthorizedMinter { - _mintInternal(qty, to, limit, proof, timestamp, signature); - } - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @notice Gets the stage info for a given stage index - /// @param index The stage index - /// @return The stage info, wallet minted count, and stage minted count - function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { - if (index >= _mintStages.length) { - revert InvalidStage(); - } - uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; - uint256 stageMinted = _stageMintedCounts[index]; - return (_mintStages[index], walletMinted, stageMinted); - } - - /// @notice Gets the mint currency address - /// @return The address of the mint currency - function getMintCurrency() external view returns (address) { - return _mintCurrency; - } - - /// @notice Gets the cosign nonce for a specific minter - /// @param minter The address of the minter - /// @return The cosign nonce - function getCosignNonce(address minter) public view returns (uint256) { - return _numberMinted(minter); - } - - /// @notice Gets the mintable status - /// @return The mintable status - function getMintable() external view returns (bool) { - return _mintable; - } - - /// @notice Gets the number of minting stages - /// @return The number of minting stages - function getNumberStages() external view override returns (uint256) { - return _mintStages.length; - } - - /// @notice Gets the maximum mintable supply - /// @return The maximum mintable supply - function getMaxMintableSupply() external view override returns (uint256) { - return _maxMintableSupply; - } - - /// @notice Gets the global wallet limit - /// @return The global wallet limit - function getGlobalWalletLimit() external view override returns (uint256) { - return _globalWalletLimit; - } - - /// @notice Gets the total minted count for a specific address - /// @param a The address to get the minted count for - /// @return The total minted count - function totalMintedByAddress(address a) external view virtual override returns (uint256) { - return _numberMinted(a); - } - - /// @notice Gets the active stage from the timestamp - /// @param timestamp The timestamp to get the active stage from - /// @return The active stage - function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { - for (uint256 i = 0; i < _mintStages.length; i++) { - if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { - return i; - } - } - revert InvalidStage(); - } - - /// @notice Checks if the contract supports a given interface - /// @param interfaceId The interface identifier - /// @return True if the contract supports the interface, false otherwise - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC2981, ERC721AUpgradeable, IERC721AUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) - || ERC721AUpgradeable.supportsInterface(interfaceId); - } - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @notice Sets up the contract with initial parameters - /// @param maxMintableSupply The maximum mintable supply - /// @param globalWalletLimit The global wallet limit - /// @param mintCurrency The address of the mint currency - /// @param fundReceiver The address to receive funds - /// @param initialStages The initial mint stages - /// @param royaltyReceiver The address to receive royalties - /// @param royaltyFeeNumerator The royalty fee numerator - function setup( - uint256 maxMintableSupply, - uint256 globalWalletLimit, - address mintCurrency, - address fundReceiver, - MintStageInfo[] calldata initialStages, - address royaltyReceiver, - uint96 royaltyFeeNumerator - ) external onlyOwner { - if (globalWalletLimit > maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _mintable = true; - _maxMintableSupply = maxMintableSupply; - _globalWalletLimit = globalWalletLimit; - _mintCurrency = mintCurrency; - _fundReceiver = fundReceiver; - _setTimestampExpirySeconds(300); // 5 minutes - - if (initialStages.length > 0) { - _setStages(initialStages); - } - - if (royaltyReceiver != address(0)) { - setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); - } - } - - /// @notice Adds an authorized minter - /// @param minter The address to add as an authorized minter - function addAuthorizedMinter(address minter) external override onlyOwner { - _addAuthorizedMinter(minter); - } - - /// @notice Removes an authorized minter - /// @param minter The address to remove as an authorized minter - function removeAuthorizedMinter(address minter) external override onlyOwner { - _removeAuthorizedMinter(minter); - } - - /// @notice Sets the cosigner address - /// @param cosigner The address to set as the cosigner - function setCosigner(address cosigner) external override onlyOwner { - _setCosigner(cosigner); - } - - /// @notice Sets the timestamp expiry seconds - /// @param timestampExpirySeconds The expiry time in seconds for timestamps - function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { - _setTimestampExpirySeconds(timestampExpirySeconds); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function setStages(MintStageInfo[] calldata newStages) external onlyOwner { - _setStages(newStages); - } - - /// @notice Sets the mintable status - /// @param mintable The mintable status to set - function setMintable(bool mintable) external onlyOwner { - _mintable = mintable; - emit SetMintable(mintable); - } - - /// @notice Sets the default royalty for the contract - /// @param receiver The address to receive royalties - /// @param feeNumerator The royalty fee numerator - function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { - super._setDefaultRoyalty(receiver, feeNumerator); - emit DefaultRoyaltySet(receiver, feeNumerator); - } - - /// @notice Sets the maximum mintable supply - /// @param maxMintableSupply The maximum mintable supply to set - function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { - if (maxMintableSupply > _maxMintableSupply) { - revert CannotIncreaseMaxMintableSupply(); - } - _maxMintableSupply = maxMintableSupply; - emit SetMaxMintableSupply(maxMintableSupply); - } - - /// @notice Sets the global wallet limit - /// @param globalWalletLimit The global wallet limit to set - function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { - if (globalWalletLimit > _maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _globalWalletLimit = globalWalletLimit; - emit SetGlobalWalletLimit(globalWalletLimit); - } - - /// @notice Allows the owner to mint tokens for a specific address - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { - _safeMint(to, qty); - } - - /// @notice Withdraws the total mint fee and remaining balance from the contract - /// @dev Can only be called by the owner - function withdraw() external onlyOwner { - (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); - if (!success) revert TransferFailed(); - _totalMintFee = 0; - - uint256 remainingValue = address(this).balance; - (success,) = _fundReceiver.call{value: remainingValue}(""); - if (!success) revert WithdrawFailed(); - - emit Withdraw(_totalMintFee + remainingValue); - } - - /// @notice Withdraws ERC20 tokens from the contract - /// @dev Can only be called by the owner - function withdrawERC20() external onlyOwner { - if (_mintCurrency == address(0)) revert WrongMintCurrency(); - - uint256 totalFee = _totalMintFee; - uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); - - if (remaining < totalFee) revert InsufficientBalance(); - - _totalMintFee = 0; - uint256 totalAmount = totalFee + remaining; - - SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); - SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); - - emit WithdrawERC20(_mintCurrency, totalAmount); - } - - /// @notice Sets the base URI for the token URIs - /// @param baseURI The base URI to set - function setBaseURI(string calldata baseURI) external onlyOwner { - _currentBaseURI = baseURI; - emit SetBaseURI(baseURI); - } - - /// @notice Sets the token URI suffix - /// @param suffix The suffix to set - function setTokenURISuffix(string calldata suffix) external onlyOwner { - _tokenURISuffix = suffix; - } - - /// @notice Sets the contract URI - /// @param uri The URI to set - function setContractURI(string calldata uri) external onlyOwner { - _contractURI = uri; - emit SetContractURI(uri); - } - - /*============================================================== - = INTERNAL HELPERS = - ==============================================================*/ - - /// @notice Internal function to handle minting logic - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function _mintInternal( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) internal canMint hasSupply(qty) { - uint256 stageTimestamp = block.timestamp; - bool waiveMintFee = false; - - if (getCosigner() != address(0)) { - waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); - _assertValidTimestamp(timestamp); - stageTimestamp = timestamp; - } - - uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); - MintStageInfo memory stage = _mintStages[activeStage]; - - uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; - - // Check value if minting with ETH - if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); - - // Check stage supply if applicable - if (stage.maxStageSupply > 0) { - if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { - revert StageSupplyExceeded(); - } - } - - // Check global wallet limit if applicable - if (_globalWalletLimit > 0) { - if (_numberMinted(to) + qty > _globalWalletLimit) { - revert WalletGlobalLimitExceeded(); - } - } - - // Check wallet limit for stage if applicable, limit == 0 means no limit enforced - if (stage.walletLimit > 0) { - if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { - revert WalletStageLimitExceeded(); - } - } - - // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required - if (stage.merkleRoot != 0) { - if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { - revert InvalidProof(); - } - - // Verify merkle proof mint limit - if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { - revert WalletStageLimitExceeded(); - } - } - - if (_mintCurrency != address(0)) { - // ERC20 mint payment - SafeTransferLib.safeTransferFrom( - _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty - ); - } - - _totalMintFee += adjustedMintFee * qty; - - _stageMintedCountsPerWallet[activeStage][to] += qty; - _stageMintedCounts[activeStage] += qty; - _safeMint(to, qty); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function _setStages(MintStageInfo[] calldata newStages) internal { - delete _mintStages; - - for (uint256 i = 0; i < newStages.length;) { - if (i >= 1) { - if ( - newStages[i].startTimeUnixSeconds - < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() - ) { - revert InsufficientStageTimeGap(); - } - } - _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); - _mintStages.push( - MintStageInfo({ - price: newStages[i].price, - mintFee: newStages[i].mintFee, - walletLimit: newStages[i].walletLimit, - merkleRoot: newStages[i].merkleRoot, - maxStageSupply: newStages[i].maxStageSupply, - startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, - endTimeUnixSeconds: newStages[i].endTimeUnixSeconds - }) - ); - emit UpdateStage( - i, - newStages[i].price, - newStages[i].mintFee, - newStages[i].walletLimit, - newStages[i].merkleRoot, - newStages[i].maxStageSupply, - newStages[i].startTimeUnixSeconds, - newStages[i].endTimeUnixSeconds - ); - - unchecked { - ++i; - } - } - } - - /// @notice Validates the start and end timestamps for a stage - /// @param start The start timestamp - /// @param end The end timestamp - function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { - if (start >= end) revert InvalidStartAndEndTimestamp(); - } - - /// @dev Overriden to prevent double-initialization of the owner. - function _guardInitializeOwner() internal pure virtual override returns (bool) { - return true; - } -} diff --git a/contracts/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol index 53e4e18..2bb5500 100644 --- a/contracts/nft/erc721m/clones/ERC721ACloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721ACloneable.sol @@ -12,9 +12,12 @@ import {Initializable} from "solady/src/utils/Initializable.sol"; * @dev Interface of ERC721 token receiver. */ interface ERC721A__IERC721Receiver { - function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) - external - returns (bytes4); + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); } /** @@ -292,9 +295,10 @@ contract ERC721ACloneable is IERC721A, Initializable { // of the XOR of all function selectors in the interface. // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) - return interfaceId == 0x01ffc9a7 // ERC165 interface ID for ERC165. - || interfaceId == 0x80ac58cd // ERC165 interface ID for ERC721. - || interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. + return + interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. + interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. + interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. } // ============================================================= @@ -322,7 +326,7 @@ contract ERC721ACloneable is IERC721A, Initializable { if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector); string memory baseURI = _baseURI(); - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ""; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ''; } /** @@ -331,7 +335,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * by default, it can be overridden in child contracts. */ function _baseURI() internal view virtual returns (string memory) { - return ""; + return ''; } // ============================================================= @@ -376,7 +380,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Initializes the ownership slot minted at `index` for efficiency purposes. */ function _initializeOwnershipAt(uint256 index) internal virtual { - if (_packedOwnerships[index] == uint256(0)) { + if (_packedOwnerships[index] == 0) { _packedOwnerships[index] = _packedOwnershipOf(index); } } @@ -394,7 +398,7 @@ contract ERC721ACloneable is IERC721A, Initializable { } // If the data at the starting slot does not exist, start the scan. - if (packed == uint256(0)) { + if (packed == 0) { if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); // Invariant: // There will always be an initialized ownership slot @@ -409,8 +413,8 @@ contract ERC721ACloneable is IERC721A, Initializable { unchecked { packed = _packedOwnerships[--tokenId]; } - if (packed == uint256(0)) continue; - if (packed & _BITMASK_BURNED == uint256(0)) return packed; + if (packed == 0) continue; + if (packed & _BITMASK_BURNED == 0) return packed; // Otherwise, the token is burned, and we must revert. // This handles the case of batch burned tokens, where only the burned bit // of the starting slot is set, and remaining slots are left uninitialized. @@ -421,7 +425,7 @@ contract ERC721ACloneable is IERC721A, Initializable { // This is possible because we have already achieved the target condition. // This saves 2143 gas on transfers of initialized tokens. // If the token is not burned, return `packed`. Otherwise, revert. - if (packed & _BITMASK_BURNED == uint256(0)) return packed; + if (packed & _BITMASK_BURNED == 0) return packed; } _revert(OwnerQueryForNonexistentToken.selector); } @@ -525,8 +529,8 @@ contract ERC721ACloneable is IERC721A, Initializable { if (tokenId < _currentIndex) { uint256 packed; - while ((packed = _packedOwnerships[tokenId]) == uint256(0)) --tokenId; - result = packed & _BITMASK_BURNED == uint256(0); + while ((packed = _packedOwnerships[tokenId]) == 0) --tokenId; + result = packed & _BITMASK_BURNED == 0; } } } @@ -545,29 +549,34 @@ contract ERC721ACloneable is IERC721A, Initializable { /** * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. */ - function _isSenderApprovedOrOwner(uint256 approvedAddressValue, uint256 ownerMasked, uint256 msgSenderMasked) - private - pure - returns (bool result) - { + function _isSenderApprovedOrOwner( + address approvedAddress, + address owner, + address msgSender + ) private pure returns (bool result) { assembly { - result := or(eq(msgSenderMasked, ownerMasked), eq(msgSenderMasked, approvedAddressValue)) + // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. + owner := and(owner, _BITMASK_ADDRESS) + // Mask `msgSender` to the lower 160 bits, in case the upper bits somehow aren't clean. + msgSender := and(msgSender, _BITMASK_ADDRESS) + // `msgSender == owner || msgSender == approvedAddress`. + result := or(eq(msgSender, owner), eq(msgSender, approvedAddress)) } } /** - * @dev Returns the storage slot and value for the approved address of `tokenId` casted to a uint256. + * @dev Returns the storage slot and value for the approved address of `tokenId`. */ - function _getApprovedSlotAndValue(uint256 tokenId) + function _getApprovedSlotAndAddress(uint256 tokenId) private view - returns (uint256 approvedAddressSlot, uint256 approvedAddressValue) + returns (uint256 approvedAddressSlot, address approvedAddress) { TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; - // The following is equivalent to `approvedAddressValue = uint160(_tokenApprovals[tokenId].value)`. + // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. assembly { approvedAddressSlot := tokenApproval.slot - approvedAddressValue := sload(approvedAddressSlot) + approvedAddress := sload(approvedAddressSlot) } } @@ -588,23 +597,32 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function transferFrom(address from, address to, uint256 tokenId) public payable virtual override { + function transferFrom( + address from, + address to, + uint256 tokenId + ) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); - uint256 fromMasked = uint160(from); - if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector); + // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. + from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); - (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + + (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) { + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); - } _beforeTokenTransfers(from, to, tokenId, 1); + // Clear approvals from the previous owner. assembly { - if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. + if approvedAddress { + // This is equivalent to `delete _tokenApprovals[tokenId]`. + sstore(approvedAddressSlot, 0) + } } // Underflow of the sender's balance is impossible because we check for @@ -620,14 +638,16 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. // - `nextInitialized` to `true`. - _packedOwnerships[tokenId] = - _packOwnershipData(to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)); + _packedOwnerships[tokenId] = _packOwnershipData( + to, + _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) + ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == uint256(0)) { + if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. @@ -637,20 +657,20 @@ contract ERC721ACloneable is IERC721A, Initializable { } } - // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint160(to); + // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; assembly { // Emit the `Transfer` event. log4( 0, // Start of data (0, since no data). 0, // End of data (0, since no data). _TRANSFER_EVENT_SIGNATURE, // Signature. - fromMasked, // `from`. + from, // `from`. toMasked, // `to`. tokenId // `tokenId`. ) } - if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector); + if (toMasked == 0) _revert(TransferToZeroAddress.selector); _afterTokenTransfers(from, to, tokenId, 1); } @@ -658,8 +678,12 @@ contract ERC721ACloneable is IERC721A, Initializable { /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom(address from, address to, uint256 tokenId) public payable virtual override { - safeTransferFrom(from, to, tokenId, ""); + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public payable virtual override { + safeTransferFrom(from, to, tokenId, ''); } /** @@ -677,162 +701,17 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) - public - payable - virtual - override - { + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) public payable virtual override { transferFrom(from, to, tokenId); - if (to.code.length != 0) { + if (to.code.length != 0) if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { _revert(TransferToNonERC721ReceiverImplementer.selector); } - } - } - - /** - * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds)`. - */ - function _batchTransferFrom(address from, address to, uint256[] memory tokenIds) internal virtual { - _batchTransferFrom(address(0), from, to, tokenIds); - } - - /** - * @dev Transfers `tokenIds` in batch from `from` to `to`. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenIds` tokens must be owned by `from`. - * - `tokenIds` must be strictly ascending. - * - If `by` is not `from`, it must be approved to move these tokens - * by either {approve} or {setApprovalForAll}. - * - * `by` is the address that to check token approval for. - * If token approval check is not needed, pass in `address(0)` for `by`. - * - * Emits a {Transfer} event for each transfer. - */ - function _batchTransferFrom(address by, address from, address to, uint256[] memory tokenIds) internal virtual { - uint256 byMasked = uint160(by); - uint256 fromMasked = uint160(from); - uint256 toMasked = uint160(to); - // Disallow transfer to zero address. - if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector); - // Whether `by` may transfer the tokens. - bool mayTransfer = _orERC721A(byMasked == uint256(0), byMasked == fromMasked) || isApprovedForAll(from, by); - - // Early return if `tokenIds` is empty. - if (tokenIds.length == uint256(0)) return; - // The next `tokenId` to be minted (i.e. `_nextTokenId()`). - uint256 end = _currentIndex; - // Pointer to start and end (exclusive) of `tokenIds`. - (uint256 ptr, uint256 ptrEnd) = _mdataERC721A(tokenIds); - - uint256 prevTokenId; - uint256 prevOwnershipPacked; - unchecked { - do { - uint256 tokenId = _mloadERC721A(ptr); - uint256 miniBatchStart = tokenId; - // Revert `tokenId` is out of bounds. - if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) { - _revert(OwnerQueryForNonexistentToken.selector); - } - // Revert if `tokenIds` is not strictly ascending. - if (prevOwnershipPacked != 0) { - if (tokenId <= prevTokenId) _revert(TokenIdsNotStrictlyAscending.selector); - } - // 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 = _packedOwnerships[j]) == uint256(0);) { - --j; - } - // If the initialized slot is burned, revert. - if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - // Check that `tokenId` is owned by `from`. - if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector); - - do { - (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); - _beforeTokenTransfers(address(uint160(fromMasked)), address(uint160(toMasked)), tokenId, 1); - // Revert if the sender is not authorized to transfer the token. - if (!mayTransfer) { - if (byMasked != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); - } - assembly { - if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. - // Emit the `Transfer` event. - log4(0, 0, _TRANSFER_EVENT_SIGNATURE, fromMasked, toMasked, tokenId) - } - - if (_mloadERC721A(ptr += 0x20) != ++tokenId) break; - if (ptr == ptrEnd) break; - } while (_packedOwnerships[tokenId] == uint256(0)); - - // Updates tokenId: - // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transferring. - // - `burned` to `false`. - // - `nextInitialized` to `false`, as it is optional. - _packedOwnerships[miniBatchStart] = _packOwnershipData( - address(uint160(toMasked)), - _nextExtraData(address(uint160(fromMasked)), address(uint160(toMasked)), prevOwnershipPacked) - ); - uint256 miniBatchLength = tokenId - miniBatchStart; - // Update the address data. - _packedAddressData[address(uint160(fromMasked))] -= miniBatchLength; - _packedAddressData[address(uint160(toMasked))] += miniBatchLength; - // Initialize the next slot if needed. - if (tokenId != end) { - if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; - } - // Perform the after hook for the batch. - _afterTokenTransfers( - address(uint160(fromMasked)), address(uint160(toMasked)), miniBatchStart, miniBatchLength - ); - // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. - prevTokenId = tokenId - 1; - } while (ptr != ptrEnd); - } - } - - /** - * @dev Safely transfers `tokenIds` in batch from `from` to `to`. - * - * Requirements: - * - * - `from` cannot be the zero address. - * - `to` cannot be the zero address. - * - `tokenIds` tokens must be owned by `from`. - * - If `by` is not `from`, it must be approved to move these tokens - * by either {approve} or {setApprovalForAll}. - * - If `to` refers to a smart contract, it must implement - * {IERC721Receiver-onERC721Received}, which is called for each transferred token. - * - * `by` is the address that to check token approval for. - * If token approval check is not needed, pass in `address(0)` for `by`. - * - * Emits a {Transfer} event for each transfer. - */ - function _safeBatchTransferFrom(address by, address from, address to, uint256[] memory tokenIds, bytes memory _data) - internal - virtual - { - _batchTransferFrom(by, from, to, tokenIds); - - unchecked { - if (to.code.length != 0) { - for ((uint256 ptr, uint256 ptrEnd) = _mdataERC721A(tokenIds); ptr != ptrEnd; ptr += 0x20) { - if (!_checkContractOnERC721Received(from, to, _mloadERC721A(ptr), _data)) { - _revert(TransferToNonERC721ReceiverImplementer.selector); - } - } - } - } } /** @@ -851,7 +730,12 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + function _beforeTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs @@ -869,7 +753,12 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ - function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} + function _afterTokenTransfers( + address from, + address to, + uint256 startTokenId, + uint256 quantity + ) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. @@ -881,16 +770,18 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Returns whether the call correctly returned the expected magic value. */ - function _checkContractOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) - private - returns (bool) - { + function _checkContractOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory _data + ) private returns (bool) { try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns ( bytes4 retval ) { return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector; } catch (bytes memory reason) { - if (reason.length == uint256(0)) { + if (reason.length == 0) { _revert(TransferToNonERC721ReceiverImplementer.selector); } assembly { @@ -915,7 +806,7 @@ contract ERC721ACloneable is IERC721A, Initializable { */ function _mint(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; - if (quantity == uint256(0)) _revert(MintZeroQuantity.selector); + if (quantity == 0) _revert(MintZeroQuantity.selector); _beforeTokenTransfers(address(0), to, startTokenId, quantity); @@ -928,8 +819,10 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. - _packedOwnerships[startTokenId] = - _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) + ); // Updates: // - `balance += quantity`. @@ -938,10 +831,10 @@ contract ERC721ACloneable is IERC721A, Initializable { // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); - // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint160(to); + // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; - if (toMasked == uint256(0)) _revert(MintToZeroAddress.selector); + if (toMasked == 0) _revert(MintToZeroAddress.selector); uint256 end = startTokenId + quantity; uint256 tokenId = startTokenId; @@ -993,7 +886,7 @@ contract ERC721ACloneable is IERC721A, Initializable { function _mintERC2309(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; if (to == address(0)) _revert(MintToZeroAddress.selector); - if (quantity == uint256(0)) _revert(MintZeroQuantity.selector); + if (quantity == 0) _revert(MintZeroQuantity.selector); if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector); _beforeTokenTransfers(address(0), to, startTokenId, quantity); @@ -1012,8 +905,10 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. - _packedOwnerships[startTokenId] = - _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) + ); if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); @@ -1037,7 +932,11 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event for each mint. */ - function _safeMint(address to, uint256 quantity, bytes memory _data) internal virtual { + function _safeMint( + address to, + uint256 quantity, + bytes memory _data + ) internal virtual { _mint(to, quantity); unchecked { @@ -1060,7 +959,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Equivalent to `_safeMint(to, quantity, '')`. */ function _safeMint(address to, uint256 quantity) internal virtual { - _safeMint(to, quantity, ""); + _safeMint(to, quantity, ''); } /** @@ -1092,8 +991,10 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `true` (as `quantity == 1`). - _packedOwnerships[tokenId] = - _packOwnershipData(to, _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked)); + _packedOwnerships[tokenId] = _packOwnershipData( + to, + _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked) + ); // Updates: // - `balance += 1`. @@ -1102,10 +1003,10 @@ contract ERC721ACloneable is IERC721A, Initializable { // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += (1 << _BITPOS_NUMBER_MINTED) | 1; - // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint160(to); + // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; - if (toMasked == uint256(0)) _revert(MintToZeroAddress.selector); + if (toMasked == 0) _revert(MintToZeroAddress.selector); assembly { // Emit the `Transfer` event. @@ -1140,7 +1041,11 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function _safeMintSpot(address to, uint256 tokenId, bytes memory _data) internal virtual { + function _safeMintSpot( + address to, + uint256 tokenId, + bytes memory _data + ) internal virtual { _mintSpot(to, tokenId); unchecked { @@ -1160,7 +1065,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Equivalent to `_safeMintSpot(to, tokenId, '')`. */ function _safeMintSpot(address to, uint256 tokenId) internal virtual { - _safeMintSpot(to, tokenId, ""); + _safeMintSpot(to, tokenId, ''); } // ============================================================= @@ -1187,14 +1092,17 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits an {Approval} event. */ - function _approve(address to, uint256 tokenId, bool approvalCheck) internal virtual { + function _approve( + address to, + uint256 tokenId, + bool approvalCheck + ) internal virtual { address owner = ownerOf(tokenId); - if (approvalCheck && _msgSenderERC721A() != owner) { + if (approvalCheck && _msgSenderERC721A() != owner) if (!isApprovedForAll(owner, _msgSenderERC721A())) { _revert(ApprovalCallerNotOwnerNorApproved.selector); } - } _tokenApprovals[tokenId].value = to; emit Approval(owner, to, tokenId); @@ -1224,22 +1132,24 @@ contract ERC721ACloneable is IERC721A, Initializable { function _burn(uint256 tokenId, bool approvalCheck) internal virtual { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); - uint256 fromMasked = uint160(prevOwnershipPacked); - address from = address(uint160(fromMasked)); + address from = address(uint160(prevOwnershipPacked)); - (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) { + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); - } } _beforeTokenTransfers(from, address(0), tokenId, 1); + // Clear approvals from the previous owner. assembly { - if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. + if approvedAddress { + // This is equivalent to `delete _tokenApprovals[tokenId]`. + sstore(approvedAddressSlot, 0) + } } // Underflow of the sender's balance is impossible because we check for @@ -1265,10 +1175,10 @@ contract ERC721ACloneable is IERC721A, Initializable { ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == uint256(0)) { + if (_packedOwnerships[nextTokenId] == 0) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. @@ -1287,101 +1197,6 @@ contract ERC721ACloneable is IERC721A, Initializable { } } - /** - * @dev Destroys `tokenIds`. - * Approvals are not cleared when tokenIds are burned. - * - * Requirements: - * - * - `tokenIds` must exist. - * - `tokenIds` must be strictly ascending. - * - `by` must be approved to burn these tokens by either {approve} or {setApprovalForAll}. - * - * `by` is the address that to check token approval for. - * If token approval check is not needed, pass in `address(0)` for `by`. - * - * Emits a {Transfer} event for each token burned. - */ - function _batchBurn(address by, uint256[] memory tokenIds) internal virtual { - // Early return if `tokenIds` is empty. - if (tokenIds.length == uint256(0)) return; - // The next `tokenId` to be minted (i.e. `_nextTokenId()`). - uint256 end = _currentIndex; - // Pointer to start and end (exclusive) of `tokenIds`. - (uint256 ptr, uint256 ptrEnd) = _mdataERC721A(tokenIds); - - uint256 prevOwnershipPacked; - address prevTokenOwner; - uint256 prevTokenId; - bool mayBurn; - unchecked { - do { - uint256 tokenId = _mloadERC721A(ptr); - uint256 miniBatchStart = tokenId; - // Revert `tokenId` is out of bounds. - if (_orERC721A(tokenId < _startTokenId(), end <= tokenId)) { - _revert(OwnerQueryForNonexistentToken.selector); - } - // Revert if `tokenIds` is not strictly ascending. - if (prevOwnershipPacked != 0) { - if (tokenId <= prevTokenId) _revert(TokenIdsNotStrictlyAscending.selector); - } - // 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 = _packedOwnerships[j]) == uint256(0);) { - --j; - } - // If the initialized slot is burned, revert. - if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); - - address tokenOwner = address(uint160(prevOwnershipPacked)); - if (tokenOwner != prevTokenOwner) { - prevTokenOwner = tokenOwner; - mayBurn = _orERC721A(by == address(0), tokenOwner == by) || isApprovedForAll(tokenOwner, by); - } - - do { - (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); - _beforeTokenTransfers(tokenOwner, address(0), tokenId, 1); - // Revert if the sender is not authorized to transfer the token. - if (!mayBurn) { - if (uint160(by) != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); - } - assembly { - if approvedAddressValue { sstore(approvedAddressSlot, 0) } // Equivalent to `delete _tokenApprovals[tokenId]`. - // Emit the `Transfer` event. - log4(0, 0, _TRANSFER_EVENT_SIGNATURE, and(_BITMASK_ADDRESS, tokenOwner), 0, tokenId) - } - if (_mloadERC721A(ptr += 0x20) != ++tokenId) break; - if (ptr == ptrEnd) break; - } while (_packedOwnerships[tokenId] == uint256(0)); - - // Updates tokenId: - // - `address` to the same `tokenOwner`. - // - `startTimestamp` to the timestamp of transferring. - // - `burned` to `true`. - // - `nextInitialized` to `false`, as it is optional. - _packedOwnerships[miniBatchStart] = _packOwnershipData( - tokenOwner, _BITMASK_BURNED | _nextExtraData(tokenOwner, address(0), prevOwnershipPacked) - ); - uint256 miniBatchLength = tokenId - miniBatchStart; - // Update the address data. - _packedAddressData[tokenOwner] += (miniBatchLength << _BITPOS_NUMBER_BURNED) - miniBatchLength; - // Initialize the next slot if needed. - if (tokenId != end) { - if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; - } - // Perform the after hook for the batch. - _afterTokenTransfers(tokenOwner, address(0), miniBatchStart, miniBatchLength); - // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. - prevTokenId = tokenId - 1; - } while (ptr != ptrEnd); - // Increment the overall burn counter. - _burnCounter += tokenIds.length; - } - } - // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= @@ -1391,7 +1206,7 @@ contract ERC721ACloneable is IERC721A, Initializable { */ function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual { uint256 packed = _packedOwnerships[index]; - if (packed == uint256(0)) _revert(OwnershipNotInitializedForExtraData.selector); + if (packed == 0) _revert(OwnershipNotInitializedForExtraData.selector); uint256 extraDataCasted; // Cast `extraData` with assembly to avoid redundant masking. assembly { @@ -1415,49 +1230,25 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _extraData(address from, address to, uint24 previousExtraData) internal view virtual returns (uint24) {} + function _extraData( + address from, + address to, + uint24 previousExtraData + ) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ - function _nextExtraData(address from, address to, uint256 prevOwnershipPacked) private view returns (uint256) { + function _nextExtraData( + address from, + address to, + uint256 prevOwnershipPacked + ) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } - // ============================================================= - // PRIVATE HELPERS - // ============================================================= - - /** - * @dev Returns a memory pointer to the start of `a`'s data. - */ - function _mdataERC721A(uint256[] memory a) private pure returns (uint256 start, uint256 end) { - assembly { - start := add(a, 0x20) - end := add(start, shl(5, mload(a))) - } - } - - /** - * @dev Returns the uint256 at `p` in memory. - */ - function _mloadERC721A(uint256 p) private pure returns (uint256 result) { - assembly { - result := mload(p) - } - } - - /** - * @dev Branchless boolean or. - */ - function _orERC721A(bool a, bool b) private pure returns (bool result) { - assembly { - result := or(iszero(iszero(a)), iszero(iszero(b))) - } - } - // ============================================================= // OTHER OPERATIONS // ============================================================= @@ -1522,4 +1313,4 @@ contract ERC721ACloneable is IERC721A, Initializable { revert(0x00, 0x04) } } -} +} \ No newline at end of file diff --git a/test/erc721m/ERC721CMInitializableTest.t.sol b/test/erc721m/ERC721CMInitializableTest.t.sol index a9f35c5..212422d 100644 --- a/test/erc721m/ERC721CMInitializableTest.t.sol +++ b/test/erc721m/ERC721CMInitializableTest.t.sol @@ -4,13 +4,23 @@ pragma solidity ^0.8.22; import {LibClone} from "solady/src/utils/LibClone.sol"; import {IERC721A} from "erc721a/contracts/IERC721A.sol"; import {Test} from "forge-std/Test.sol"; -import {ERC721CMInitializableV1_0_0 as ERC721CMInitializable} from - "../../contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol"; +import {ERC721CMInitializableV1_0_1 as ERC721CMInitializable} from + "../../contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol"; import {MintStageInfo} from "../../contracts/common/Structs.sol"; import {ErrorsAndEvents} from "../../contracts/common/ErrorsAndEvents.sol"; +contract MockERC721CMInitializable is ERC721CMInitializable { + function baseURI() public view returns (string memory) { + return _currentBaseURI; + } + + function tokenURISuffix() public view returns (string memory) { + return _tokenURISuffix; + } +} + contract ERC721CMInitializableTest is Test { - ERC721CMInitializable public nft; + MockERC721CMInitializable public nft; address public owner; address public minter; address public fundReceiver; @@ -27,11 +37,19 @@ contract ERC721CMInitializableTest is Test { vm.deal(owner, 10 ether); vm.deal(minter, 2 ether); - address clone = LibClone.deployERC1967(address(new ERC721CMInitializable())); - nft = ERC721CMInitializable(clone); + address clone = LibClone.deployERC1967(address(new MockERC721CMInitializable())); + nft = MockERC721CMInitializable(clone); nft.initialize("Test", "TEST", owner); nft.setup( - INITIAL_SUPPLY, GLOBAL_WALLET_LIMIT, address(0), fundReceiver, new MintStageInfo[](0), address(this), 0 + "base_uri_", + ".json", + INITIAL_SUPPLY, + GLOBAL_WALLET_LIMIT, + address(0), + fundReceiver, + new MintStageInfo[](0), + address(this), + 0 ); } @@ -169,4 +187,12 @@ contract ERC721CMInitializableTest is Test { nft.setContractURI(uri); assertEq(nft.contractURI(), uri); } + + function testBaseURISetup() public { + assertEq(nft.baseURI(), "base_uri_"); + } + + function testTokenURISuffixSetup() public { + assertEq(nft.tokenURISuffix(), ".json"); + } } From 5a6110fac5f89417b0ee8119904260f259f9d8c8 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 17:58:21 -0500 Subject: [PATCH 05/26] fmt Signed-off-by: Adam Wolf --- .../nft/erc721m/clones/ERC721ACloneable.sol | 156 ++++++------------ .../ERC721AConduitPreapprovedCloneable.sol | 6 +- .../ERC721MagicDropMetadataCloneable.sol | 32 +++- .../interfaces/IERC721MagicDropMetadata.sol | 4 +- 4 files changed, 83 insertions(+), 115 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol index 2bb5500..cb18bda 100644 --- a/contracts/nft/erc721m/clones/ERC721ACloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721ACloneable.sol @@ -12,12 +12,9 @@ import {Initializable} from "solady/src/utils/Initializable.sol"; * @dev Interface of ERC721 token receiver. */ interface ERC721A__IERC721Receiver { - function onERC721Received( - address operator, - address from, - uint256 tokenId, - bytes calldata data - ) external returns (bytes4); + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + returns (bytes4); } /** @@ -143,10 +140,10 @@ contract ERC721ACloneable is IERC721A, Initializable { uint256 private _spotMinted; // ============================================================= - // CONSTRUCTOR + // INITIALIZER // ============================================================= - constructor(string memory name_, string memory symbol_) { + function __ERC721ACloneable__init(string memory name_, string memory symbol_) internal onlyInitializing { _name = name_; _symbol = symbol_; _currentIndex = _startTokenId(); @@ -295,10 +292,9 @@ contract ERC721ACloneable is IERC721A, Initializable { // of the XOR of all function selectors in the interface. // See: [ERC165](https://eips.ethereum.org/EIPS/eip-165) // (e.g. `bytes4(i.functionA.selector ^ i.functionB.selector ^ ...)`) - return - interfaceId == 0x01ffc9a7 || // ERC165 interface ID for ERC165. - interfaceId == 0x80ac58cd || // ERC165 interface ID for ERC721. - interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. + return interfaceId == 0x01ffc9a7 // ERC165 interface ID for ERC165. + || interfaceId == 0x80ac58cd // ERC165 interface ID for ERC721. + || interfaceId == 0x5b5e139f; // ERC165 interface ID for ERC721Metadata. } // ============================================================= @@ -326,7 +322,7 @@ contract ERC721ACloneable is IERC721A, Initializable { if (!_exists(tokenId)) _revert(URIQueryForNonexistentToken.selector); string memory baseURI = _baseURI(); - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ''; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId))) : ""; } /** @@ -335,7 +331,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * by default, it can be overridden in child contracts. */ function _baseURI() internal view virtual returns (string memory) { - return ''; + return ""; } // ============================================================= @@ -549,11 +545,11 @@ contract ERC721ACloneable is IERC721A, Initializable { /** * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. */ - function _isSenderApprovedOrOwner( - address approvedAddress, - address owner, - address msgSender - ) private pure returns (bool result) { + function _isSenderApprovedOrOwner(address approvedAddress, address owner, address msgSender) + private + pure + returns (bool result) + { assembly { // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. owner := and(owner, _BITMASK_ADDRESS) @@ -597,11 +593,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function transferFrom( - address from, - address to, - uint256 tokenId - ) public payable virtual override { + function transferFrom(address from, address to, uint256 tokenId) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. @@ -612,8 +604,9 @@ contract ERC721ACloneable is IERC721A, Initializable { (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) { if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + } _beforeTokenTransfers(from, to, tokenId, 1); @@ -638,10 +631,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of transfering. // - `burned` to `false`. // - `nextInitialized` to `true`. - _packedOwnerships[tokenId] = _packOwnershipData( - to, - _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) - ); + _packedOwnerships[tokenId] = + _packOwnershipData(to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked)); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { @@ -678,12 +669,8 @@ contract ERC721ACloneable is IERC721A, Initializable { /** * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public payable virtual override { - safeTransferFrom(from, to, tokenId, ''); + function safeTransferFrom(address from, address to, uint256 tokenId) public payable virtual override { + safeTransferFrom(from, to, tokenId, ""); } /** @@ -701,17 +688,18 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function safeTransferFrom( - address from, - address to, - uint256 tokenId, - bytes memory _data - ) public payable virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) + public + payable + virtual + override + { transferFrom(from, to, tokenId); - if (to.code.length != 0) + if (to.code.length != 0) { if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { _revert(TransferToNonERC721ReceiverImplementer.selector); } + } } /** @@ -730,12 +718,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _beforeTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Hook that is called after a set of serially-ordered token IDs @@ -753,12 +736,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` has been burned by `from`. * - `from` and `to` are never both zero. */ - function _afterTokenTransfers( - address from, - address to, - uint256 startTokenId, - uint256 quantity - ) internal virtual {} + function _afterTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) internal virtual {} /** * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target contract. @@ -770,12 +748,10 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Returns whether the call correctly returned the expected magic value. */ - function _checkContractOnERC721Received( - address from, - address to, - uint256 tokenId, - bytes memory _data - ) private returns (bool) { + function _checkContractOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) + private + returns (bool) + { try ERC721A__IERC721Receiver(to).onERC721Received(_msgSenderERC721A(), from, tokenId, _data) returns ( bytes4 retval ) { @@ -819,10 +795,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) - ); + _packedOwnerships[startTokenId] = + _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); // Updates: // - `balance += quantity`. @@ -905,10 +879,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `quantity == 1`. - _packedOwnerships[startTokenId] = _packOwnershipData( - to, - _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0) - ); + _packedOwnerships[startTokenId] = + _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); @@ -932,11 +904,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event for each mint. */ - function _safeMint( - address to, - uint256 quantity, - bytes memory _data - ) internal virtual { + function _safeMint(address to, uint256 quantity, bytes memory _data) internal virtual { _mint(to, quantity); unchecked { @@ -959,7 +927,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Equivalent to `_safeMint(to, quantity, '')`. */ function _safeMint(address to, uint256 quantity) internal virtual { - _safeMint(to, quantity, ''); + _safeMint(to, quantity, ""); } /** @@ -991,10 +959,8 @@ contract ERC721ACloneable is IERC721A, Initializable { // - `startTimestamp` to the timestamp of minting. // - `burned` to `false`. // - `nextInitialized` to `true` (as `quantity == 1`). - _packedOwnerships[tokenId] = _packOwnershipData( - to, - _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked) - ); + _packedOwnerships[tokenId] = + _packOwnershipData(to, _nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked)); // Updates: // - `balance += 1`. @@ -1041,11 +1007,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits a {Transfer} event. */ - function _safeMintSpot( - address to, - uint256 tokenId, - bytes memory _data - ) internal virtual { + function _safeMintSpot(address to, uint256 tokenId, bytes memory _data) internal virtual { _mintSpot(to, tokenId); unchecked { @@ -1065,7 +1027,7 @@ contract ERC721ACloneable is IERC721A, Initializable { * @dev Equivalent to `_safeMintSpot(to, tokenId, '')`. */ function _safeMintSpot(address to, uint256 tokenId) internal virtual { - _safeMintSpot(to, tokenId, ''); + _safeMintSpot(to, tokenId, ""); } // ============================================================= @@ -1092,17 +1054,14 @@ contract ERC721ACloneable is IERC721A, Initializable { * * Emits an {Approval} event. */ - function _approve( - address to, - uint256 tokenId, - bool approvalCheck - ) internal virtual { + function _approve(address to, uint256 tokenId, bool approvalCheck) internal virtual { address owner = ownerOf(tokenId); - if (approvalCheck && _msgSenderERC721A() != owner) + if (approvalCheck && _msgSenderERC721A() != owner) { if (!isApprovedForAll(owner, _msgSenderERC721A())) { _revert(ApprovalCallerNotOwnerNorApproved.selector); } + } _tokenApprovals[tokenId].value = to; emit Approval(owner, to, tokenId); @@ -1138,8 +1097,9 @@ contract ERC721ACloneable is IERC721A, Initializable { if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) { if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + } } _beforeTokenTransfers(from, address(0), tokenId, 1); @@ -1230,21 +1190,13 @@ contract ERC721ACloneable is IERC721A, Initializable { * - When `to` is zero, `tokenId` will be burned by `from`. * - `from` and `to` are never both zero. */ - function _extraData( - address from, - address to, - uint24 previousExtraData - ) internal view virtual returns (uint24) {} + function _extraData(address from, address to, uint24 previousExtraData) internal view virtual returns (uint24) {} /** * @dev Returns the next extra data for the packed ownership data. * The returned result is shifted into position. */ - function _nextExtraData( - address from, - address to, - uint256 prevOwnershipPacked - ) private view returns (uint256) { + function _nextExtraData(address from, address to, uint256 prevOwnershipPacked) private view returns (uint256) { uint24 extraData = uint24(prevOwnershipPacked >> _BITPOS_EXTRA_DATA); return uint256(_extraData(from, to, extraData)) << _BITPOS_EXTRA_DATA; } @@ -1313,4 +1265,4 @@ contract ERC721ACloneable is IERC721A, Initializable { revert(0x00, 0x04) } } -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol index 2873626..573dcac 100644 --- a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; -import {ERC721A} from "erc721a/contracts/ERC721A.sol"; +import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {IERC721A} from "erc721a/contracts/IERC721A.sol"; /** @@ -21,12 +21,12 @@ abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneabl public view virtual - override(ERC721A, IERC721A) + override(ERC721ACloneable, IERC721A) returns (bool) { if (operator == _CONDUIT) { return true; } - return ERC721A.isApprovedForAll(owner, operator); + return ERC721ACloneable.isApprovedForAll(owner, operator); } } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 8907a7a..889ae95 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -4,7 +4,10 @@ pragma solidity ^0.8.22; import {ERC2981} from "solady/src/tokens/ERC2981.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; +import {IERC721A} from "erc721a/contracts/IERC721A.sol"; + import {ERC721AConduitPreapprovedCloneable} from "./ERC721AConduitPreapprovedCloneable.sol"; +import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; @@ -80,14 +83,29 @@ contract ERC721MagicDropMetadata is ERC721AConduitPreapprovedCloneable, IERC721M return _royaltyBps; } + /// @notice Returns true if the contract implements the interface + /// @param interfaceId The interface ID to check + /// @return True if the contract implements the interface + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721ACloneable, IERC721A, ERC2981) + returns (bool) + { + return interfaceId == 0x2a55205a // ERC-2981 + || interfaceId == 0x49064906 // ERC-4906 + || super.supportsInterface(interfaceId); + } + /*============================================================== = ADMIN OPERATIONS = ==============================================================*/ /// @notice Sets the base URI for the token URIs - /// @param baseURI The base URI to set - function setBaseURI(string calldata baseURI) external override onlyOwner { - _tokenBaseURI = baseURI; + /// @param newBaseURI The base URI to set + function setBaseURI(string calldata newBaseURI) external override onlyOwner { + _tokenBaseURI = newBaseURI; if (totalSupply() != 0) { emit BatchMetadataUpdate(0, totalSupply() - 1); @@ -95,11 +113,11 @@ contract ERC721MagicDropMetadata is ERC721AConduitPreapprovedCloneable, IERC721M } /// @notice Sets the contract URI for contract metadata - /// @param contractURI The contract URI to set - function setContractURI(string calldata contractURI) external override onlyOwner { - _contractURI = contractURI; + /// @param newContractURI The contract URI to set + function setContractURI(string calldata newContractURI) external override onlyOwner { + _contractURI = newContractURI; - emit ContractURIUpdated(contractURI); + emit ContractURIUpdated(newContractURI); } /// @notice Sets the max supply of tokens to be minted diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol index 9bc287f..1286a32 100644 --- a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol +++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; -import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; - -interface IERC721MagicDropMetadata is IERC2981 { +interface IERC721MagicDropMetadata { /*============================================================== = EVENTS = ==============================================================*/ From 4ad4cdcee6acc94fbd95636f7795b2fb3eeba0d4 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 18:58:59 -0500 Subject: [PATCH 06/26] cloneable Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.sol | 226 ++++++++++++++++++ .../ERC721MagicDropMetadataCloneable.sol | 7 +- 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol new file mode 100644 index 0000000..1e58e1e --- /dev/null +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; + +import {IERC721A} from "erc721a/contracts/IERC721A.sol"; + +import {ERC721MagicDropMetadataCloneable} from "./ERC721MagicDropMetadataCloneable.sol"; +import {ERC721ACloneable} from "./ERC721ACloneable.sol"; +import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; + +contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, ReentrancyGuard { + address private _payoutRecipient; + address private _feeRecipient; + uint256 private _mintFee; + uint256 private _totalMintFee; + + PublicStage private _publicStage; + AllowlistStage private _allowlistStage; + + struct PublicStage { + uint256 startTime; + uint256 endTime; + uint256 price; + } + + struct AllowlistStage { + uint256 startTime; + uint256 endTime; + uint256 price; + bytes32 merkleRoot; + } + + struct SetupConfig { + uint256 maxSupply; + uint256 walletLimit; + string baseURI; + string contractURI; + PublicStage publicStage; + AllowlistStage allowlistStage; + address payoutRecipient; + address feeRecipient; + uint256 mintFee; + bytes32 provenanceHash; + } + + error PublicStageNotActive(); + error AllowlistStageNotActive(); + error NotEnoughValue(); + error WalletLimitExceeded(); + error WithdrawFailed(); + error InvalidProof(); + + event MagicDropTokenDeployed(); + event Withdraw(uint256 amount); + + function initialize(string memory _name, string memory _symbol, address _owner) public initializer { + __ERC721ACloneable__init(_name, _symbol); + _initializeOwner(_owner); + + emit MagicDropTokenDeployed(); + } + + function burn(uint256 tokenId) external { + _burn(tokenId, true); + } + + function _startTokenId() internal view virtual override returns (uint256) { + return 1; + } + + function tokenURI(uint256 tokenId) + public + view + virtual + override(ERC721ACloneable, IERC721A) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + bool isBaseURIEmpty = bytes(baseURI).length == 0; + bool hasNoTrailingSlash = bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]; + + if (isBaseURIEmpty) { + return ""; + } + + if (hasNoTrailingSlash) { + return baseURI; + } + + return string(abi.encodePacked(baseURI, _toString(tokenId))); + } + + function setPublicStage(PublicStage calldata stage) external onlyOwner { + _publicStage = stage; + } + + function setAllowlistStage(AllowlistStage calldata stage) external onlyOwner { + _allowlistStage = stage; + } + + function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { + _payoutRecipient = newPayoutRecipient; + } + + function mintPublic(uint256 qty, address to) external payable { + PublicStage memory stage = _publicStage; + if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { + revert PublicStageNotActive(); + } + + uint256 mintFees = _mintFee * qty; + + if (msg.value < stage.price * qty + mintFees) { + revert NotEnoughValue(); + } + + if (_numberMinted(to) + qty > this.walletLimit()) { + revert WalletLimitExceeded(); + } + + _totalMintFee += mintFees; + + _safeMint(to, qty); + } + + function mintAllowlist(uint256 qty, address to, bytes32[] calldata proof) external payable { + AllowlistStage memory stage = _allowlistStage; + if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { + revert AllowlistStageNotActive(); + } + + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to)))) { + revert InvalidProof(); + } + + uint256 mintFees = _mintFee * qty; + + if (msg.value < stage.price * qty + mintFees) { + revert NotEnoughValue(); + } + + if (_numberMinted(to) + qty > this.walletLimit()) { + revert WalletLimitExceeded(); + } + + _totalMintFee += mintFees; + + _safeMint(to, qty); + } + + function mintFee() external view returns (uint256) { + return _mintFee; + } + + function setup(SetupConfig calldata config) external onlyOwner { + if (config.maxSupply > 0) { + this.setMaxSupply(config.maxSupply); + } + + if (config.walletLimit > 0) { + this.setWalletLimit(config.walletLimit); + } + + if (bytes(config.baseURI).length > 0) { + this.setBaseURI(config.baseURI); + } + + if (bytes(config.contractURI).length > 0) { + this.setContractURI(config.contractURI); + } + + if (config.publicStage.startTime != 0 || config.publicStage.endTime != 0) { + this.setPublicStage(config.publicStage); + } + + if (config.allowlistStage.startTime != 0 || config.allowlistStage.endTime != 0) { + this.setAllowlistStage(config.allowlistStage); + } + + if (config.payoutRecipient != address(0)) { + this.setPayoutRecipient(config.payoutRecipient); + } + + if (config.feeRecipient != address(0)) { + _feeRecipient = config.feeRecipient; + } + + if (config.provenanceHash != bytes32(0)) { + this.setProvenanceHash(config.provenanceHash); + } + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721MagicDropMetadataCloneable) + returns (bool) + { + return interfaceId == type(IERC721MagicDropMetadata).interfaceId || super.supportsInterface(interfaceId); + } + + /// @notice Withdraws the total mint fee and remaining balance from the contract + /// @dev Can only be called by the owner + function withdraw() external onlyOwner { + (bool success,) = _feeRecipient.call{value: _totalMintFee}(""); + if (!success) revert WithdrawFailed(); + _totalMintFee = 0; + + uint256 remainingValue = address(this).balance; + (success,) = _payoutRecipient.call{value: remainingValue}(""); + if (!success) revert WithdrawFailed(); + + emit Withdraw(_totalMintFee + remainingValue); + } + + /// @dev Overriden to prevent double-initialization of the owner. + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } +} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 889ae95..0d262f7 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -11,7 +11,12 @@ import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; -contract ERC721MagicDropMetadata is ERC721AConduitPreapprovedCloneable, IERC721MagicDropMetadata, ERC2981, Ownable { +contract ERC721MagicDropMetadataCloneable is + ERC721AConduitPreapprovedCloneable, + IERC721MagicDropMetadata, + ERC2981, + Ownable +{ /*============================================================== = STORAGE = ==============================================================*/ From 3df1b0ae528c4b1b44303eaf47e030b1d9a8e6ad Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Fri, 6 Dec 2024 10:44:54 -0500 Subject: [PATCH 07/26] move contract version to another branch Signed-off-by: Adam Wolf --- .../erc721m/ERC721CMInitializableV1_0_1.sol | 545 ----------------- .../erc721m/ERC721MInitializableV1_0_1.sol | 578 ------------------ test/erc721m/ERC721CMInitializableTest.t.sol | 28 +- test/erc721m/ERC721MInitializableTest.t.sol | 100 --- 4 files changed, 4 insertions(+), 1247 deletions(-) delete mode 100644 contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol delete mode 100644 contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol delete mode 100644 test/erc721m/ERC721MInitializableTest.t.sol diff --git a/contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol b/contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol deleted file mode 100644 index caf1489..0000000 --- a/contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol +++ /dev/null @@ -1,545 +0,0 @@ -//SPDX-License-Identifier: MIT - -pragma solidity ^0.8.22; - -import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; -import {ERC2981} from "solady/src/tokens/ERC2981.sol"; -import {Ownable} from "solady/src/auth/Ownable.sol"; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; -import { - ERC721ACQueryableInitializable, - ERC721AUpgradeable, - IERC721AUpgradeable -} from "../creator-token-standards/ERC721ACQueryableInitializable.sol"; -import {ERC721MStorage} from "./ERC721MStorage.sol"; -import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; -import {MintStageInfo} from "../../common/Structs.sol"; -import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; -import {Cosignable} from "../../common/Cosignable.sol"; -import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; - -/** - * @title ERC721CMInitializableV1_0_1 - * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this - * contract is for use with EIP-1167 Minimal Proxies (Clones). - */ -contract ERC721CMInitializableV1_0_1 is - IERC721MInitializable, - ERC721ACQueryableInitializable, - ERC2981, - Ownable, - ReentrancyGuard, - Cosignable, - AuthorizedMinterControl, - ERC721MStorage -{ - /*============================================================== - = INITIALIZERS = - ==============================================================*/ - - constructor() { - _disableInitializers(); - } - - /// @notice Initializes the contract - /// @param name The name of the token collection - /// @param symbol The symbol of the token collection - /// @param initialOwner The address of the initial owner - function initialize(string calldata name, string calldata symbol, address initialOwner) - external - initializer - initializerERC721A - { - if (initialOwner == address(0)) { - revert InitialOwnerCannotBeZero(); - } - - __ERC721ACQueryableInitializable_init(name, symbol); - _initializeOwner(initialOwner); - } - - /*============================================================== - = META = - ==============================================================*/ - - /// @notice Returns the contract name and version - /// @return The contract name and version as strings - function contractNameAndVersion() public pure returns (string memory, string memory) { - return ("ERC721CMInitializable", "1.0.1"); - } - - /// @notice Gets the token URI for a specific token ID - /// @param tokenId The ID of the token - /// @return The token URI - function tokenURI(uint256 tokenId) - public - view - override(ERC721AUpgradeable, IERC721AUpgradeable) - returns (string memory) - { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _currentBaseURI; - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; - } - - /// @notice Gets the contract URI - /// @return The contract URI - function contractURI() public view returns (string memory) { - return _contractURI; - } - - /*============================================================== - = MODIFIERS = - ==============================================================*/ - - /// @notice Modifier to check if the contract is mintable - modifier canMint() { - if (!_mintable) revert NotMintable(); - _; - } - - /// @notice Modifier to check if the total supply is enough - /// @param qty The quantity to mint - modifier hasSupply(uint256 qty) { - if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); - _; - } - - /*============================================================== - = PUBLIC WRITE METHODS = - ==============================================================*/ - - /// @notice Mints tokens for the caller - /// @param qty The quantity to mint - /// @param limit The minting limit for the caller (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) - external - payable - virtual - nonReentrant - { - _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); - } - - /// @notice Allows authorized minters to mint tokens for a specified address - /// @param to The address to mint tokens for - /// @param qty The quantity to mint - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function authorizedMint( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) external payable onlyAuthorizedMinter { - _mintInternal(qty, to, limit, proof, timestamp, signature); - } - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @notice Gets the stage info for a given stage index - /// @param index The stage index - /// @return The stage info, wallet minted count, and stage minted count - function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { - if (index >= _mintStages.length) { - revert InvalidStage(); - } - uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; - uint256 stageMinted = _stageMintedCounts[index]; - return (_mintStages[index], walletMinted, stageMinted); - } - - /// @notice Gets the mint currency address - /// @return The address of the mint currency - function getMintCurrency() external view returns (address) { - return _mintCurrency; - } - - /// @notice Gets the cosign nonce for a specific minter - /// @param minter The address of the minter - /// @return The cosign nonce - function getCosignNonce(address minter) public view returns (uint256) { - return _numberMinted(minter); - } - - /// @notice Gets the mintable status - /// @return The mintable status - function getMintable() external view returns (bool) { - return _mintable; - } - - /// @notice Gets the number of minting stages - /// @return The number of minting stages - function getNumberStages() external view override returns (uint256) { - return _mintStages.length; - } - - /// @notice Gets the maximum mintable supply - /// @return The maximum mintable supply - function getMaxMintableSupply() external view override returns (uint256) { - return _maxMintableSupply; - } - - /// @notice Gets the global wallet limit - /// @return The global wallet limit - function getGlobalWalletLimit() external view override returns (uint256) { - return _globalWalletLimit; - } - - /// @notice Gets the total minted count for a specific address - /// @param a The address to get the minted count for - /// @return The total minted count - function totalMintedByAddress(address a) external view virtual override returns (uint256) { - return _numberMinted(a); - } - - /// @notice Gets the active stage from the timestamp - /// @param timestamp The timestamp to get the active stage from - /// @return The active stage - function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { - for (uint256 i = 0; i < _mintStages.length; i++) { - if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { - return i; - } - } - revert InvalidStage(); - } - - /// @notice Checks if the contract supports a given interface - /// @param interfaceId The interface identifier - /// @return True if the contract supports the interface, false otherwise - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC2981, IERC721AUpgradeable, ERC721ACQueryableInitializable) - returns (bool) - { - return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) - || ERC721ACQueryableInitializable.supportsInterface(interfaceId); - } - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @notice Sets up the contract with initial parameters - /// @param baseURI The base URI for the token URIs - /// @param tokenURISuffix The suffix for the token URIs - /// @param maxMintableSupply The maximum mintable supply - /// @param globalWalletLimit The global wallet limit - /// @param mintCurrency The address of the mint currency - /// @param fundReceiver The address to receive funds - /// @param initialStages The initial mint stages - /// @param royaltyReceiver The address to receive royalties - /// @param royaltyFeeNumerator The royalty fee numerator - function setup( - string calldata baseURI, - string calldata tokenURISuffix, - uint256 maxMintableSupply, - uint256 globalWalletLimit, - address mintCurrency, - address fundReceiver, - MintStageInfo[] calldata initialStages, - address royaltyReceiver, - uint96 royaltyFeeNumerator - ) external onlyOwner { - if (globalWalletLimit > maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _mintable = true; - _maxMintableSupply = maxMintableSupply; - _globalWalletLimit = globalWalletLimit; - _mintCurrency = mintCurrency; - _fundReceiver = fundReceiver; - _currentBaseURI = baseURI; - _tokenURISuffix = tokenURISuffix; - _setTimestampExpirySeconds(300); // 5 minutes - - if (initialStages.length > 0) { - _setStages(initialStages); - } - - if (royaltyReceiver != address(0)) { - setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); - } - } - - /// @notice Adds an authorized minter - /// @param minter The address to add as an authorized minter - function addAuthorizedMinter(address minter) external override onlyOwner { - _addAuthorizedMinter(minter); - } - - /// @notice Removes an authorized minter - /// @param minter The address to remove as an authorized minter - function removeAuthorizedMinter(address minter) external override onlyOwner { - _removeAuthorizedMinter(minter); - } - - /// @notice Sets the cosigner address - /// @param cosigner The address to set as the cosigner - function setCosigner(address cosigner) external override onlyOwner { - _setCosigner(cosigner); - } - - /// @notice Sets the timestamp expiry seconds - /// @param timestampExpirySeconds The expiry time in seconds for timestamps - function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { - _setTimestampExpirySeconds(timestampExpirySeconds); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function setStages(MintStageInfo[] calldata newStages) external onlyOwner { - _setStages(newStages); - } - - /// @notice Sets the mintable status - /// @param mintable The mintable status to set - function setMintable(bool mintable) external onlyOwner { - _mintable = mintable; - emit SetMintable(mintable); - } - - /// @notice Sets the default royalty for the contract - /// @param receiver The address to receive royalties - /// @param feeNumerator The royalty fee numerator - function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { - super._setDefaultRoyalty(receiver, feeNumerator); - emit DefaultRoyaltySet(receiver, feeNumerator); - } - - /// @notice Sets the maximum mintable supply - /// @param maxMintableSupply The maximum mintable supply to set - function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { - if (maxMintableSupply > _maxMintableSupply) { - revert CannotIncreaseMaxMintableSupply(); - } - _maxMintableSupply = maxMintableSupply; - emit SetMaxMintableSupply(maxMintableSupply); - } - - /// @notice Sets the global wallet limit - /// @param globalWalletLimit The global wallet limit to set - function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { - if (globalWalletLimit > _maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _globalWalletLimit = globalWalletLimit; - emit SetGlobalWalletLimit(globalWalletLimit); - } - - /// @notice Allows the owner to mint tokens for a specific address - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { - _safeMint(to, qty); - } - - /// @notice Withdraws the total mint fee and remaining balance from the contract - /// @dev Can only be called by the owner - function withdraw() external onlyOwner { - (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); - if (!success) revert TransferFailed(); - _totalMintFee = 0; - - uint256 remainingValue = address(this).balance; - (success,) = _fundReceiver.call{value: remainingValue}(""); - if (!success) revert WithdrawFailed(); - - emit Withdraw(_totalMintFee + remainingValue); - } - - /// @notice Withdraws ERC20 tokens from the contract - /// @dev Can only be called by the owner - function withdrawERC20() external onlyOwner { - if (_mintCurrency == address(0)) revert WrongMintCurrency(); - - uint256 totalFee = _totalMintFee; - uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); - - if (remaining < totalFee) revert InsufficientBalance(); - - _totalMintFee = 0; - uint256 totalAmount = totalFee + remaining; - - SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); - SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); - - emit WithdrawERC20(_mintCurrency, totalAmount); - } - - /// @notice Sets the base URI for the token URIs - /// @param baseURI The base URI to set - function setBaseURI(string calldata baseURI) external onlyOwner { - _currentBaseURI = baseURI; - emit SetBaseURI(baseURI); - } - - /// @notice Sets the token URI suffix - /// @param suffix The suffix to set - function setTokenURISuffix(string calldata suffix) external onlyOwner { - _tokenURISuffix = suffix; - } - - /// @notice Sets the contract URI - /// @param uri The URI to set - function setContractURI(string calldata uri) external onlyOwner { - _contractURI = uri; - emit SetContractURI(uri); - } - - /*============================================================== - = INTERNAL HELPERS = - ==============================================================*/ - - /// @notice Internal function to handle minting logic - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function _mintInternal( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) internal canMint hasSupply(qty) { - uint256 stageTimestamp = block.timestamp; - bool waiveMintFee = false; - - if (getCosigner() != address(0)) { - waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); - _assertValidTimestamp(timestamp); - stageTimestamp = timestamp; - } - - uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); - MintStageInfo memory stage = _mintStages[activeStage]; - - uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; - - // Check value if minting with ETH - if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); - - // Check stage supply if applicable - if (stage.maxStageSupply > 0) { - if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { - revert StageSupplyExceeded(); - } - } - - // Check global wallet limit if applicable - if (_globalWalletLimit > 0) { - if (_numberMinted(to) + qty > _globalWalletLimit) { - revert WalletGlobalLimitExceeded(); - } - } - - // Check wallet limit for stage if applicable, limit == 0 means no limit enforced - if (stage.walletLimit > 0) { - if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { - revert WalletStageLimitExceeded(); - } - } - - // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required - if (stage.merkleRoot != 0) { - if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { - revert InvalidProof(); - } - - // Verify merkle proof mint limit - if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { - revert WalletStageLimitExceeded(); - } - } - - if (_mintCurrency != address(0)) { - // ERC20 mint payment - SafeTransferLib.safeTransferFrom( - _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty - ); - } - - _totalMintFee += adjustedMintFee * qty; - - _stageMintedCountsPerWallet[activeStage][to] += qty; - _stageMintedCounts[activeStage] += qty; - _safeMint(to, qty); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function _setStages(MintStageInfo[] calldata newStages) internal { - delete _mintStages; - - for (uint256 i = 0; i < newStages.length;) { - if (i >= 1) { - if ( - newStages[i].startTimeUnixSeconds - < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() - ) { - revert InsufficientStageTimeGap(); - } - } - _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); - _mintStages.push( - MintStageInfo({ - price: newStages[i].price, - mintFee: newStages[i].mintFee, - walletLimit: newStages[i].walletLimit, - merkleRoot: newStages[i].merkleRoot, - maxStageSupply: newStages[i].maxStageSupply, - startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, - endTimeUnixSeconds: newStages[i].endTimeUnixSeconds - }) - ); - emit UpdateStage( - i, - newStages[i].price, - newStages[i].mintFee, - newStages[i].walletLimit, - newStages[i].merkleRoot, - newStages[i].maxStageSupply, - newStages[i].startTimeUnixSeconds, - newStages[i].endTimeUnixSeconds - ); - - unchecked { - ++i; - } - } - } - - /// @notice Validates the start and end timestamps for a stage - /// @param start The start timestamp - /// @param end The end timestamp - function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { - if (start >= end) revert InvalidStartAndEndTimestamp(); - } - - /// @notice Requires the caller to be the contract owner - function _requireCallerIsContractOwner() internal view override { - return _checkOwner(); - } - - /// @dev Overriden to prevent double-initialization of the owner. - function _guardInitializeOwner() internal pure virtual override returns (bool) { - return true; - } -} diff --git a/contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol b/contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol deleted file mode 100644 index 38d74f4..0000000 --- a/contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol +++ /dev/null @@ -1,578 +0,0 @@ -//SPDX-License-Identifier: MIT - -pragma solidity ^0.8.22; - -import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; -import {ERC2981} from "solady/src/tokens/ERC2981.sol"; -import {Ownable} from "solady/src/auth/Ownable.sol"; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; -import {Initializable} from "solady/src/utils/Initializable.sol"; - -import {ERC721AUpgradeable, IERC721AUpgradeable} from "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; -import { - ERC721AQueryableUpgradeable, - IERC721AQueryableUpgradeable -} from "erc721a-upgradeable/contracts/extensions/ERC721AQueryableUpgradeable.sol"; -import {IERC721A, ERC721A} from "erc721a/contracts/extensions/ERC721AQueryable.sol"; -import {ERC721MStorage} from "./ERC721MStorage.sol"; -import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; -import {MintStageInfo} from "../../common/Structs.sol"; -import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; -import {Cosignable} from "../../common/Cosignable.sol"; -import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; - -/** - * @title ERC721MInitializableV1_0_1 - * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this - * contract is for use with EIP-1167 Minimal Proxies (Clones). - */ -contract ERC721MInitializableV1_0_1 is - IERC721MInitializable, - ERC721AQueryableUpgradeable, - ERC2981, - Ownable, - ReentrancyGuard, - Cosignable, - AuthorizedMinterControl, - ERC721MStorage, - Initializable -{ - /*============================================================== - = STORAGE = - ==============================================================*/ - bool public frozen; - - /*============================================================== - = EVENTS = - ==============================================================*/ - event SetFrozen(bool frozen); - - /*============================================================== - = ERRORS = - ==============================================================*/ - error TransfersAreFrozen(); - - /*============================================================== - = INITIALIZERS = - ==============================================================*/ - - constructor() { - _disableInitializers(); - } - - /// @notice Initializes the contract - /// @param name The name of the token collection - /// @param symbol The symbol of the token collection - /// @param initialOwner The address of the initial owner - function initialize(string calldata name, string calldata symbol, address initialOwner) - external - initializer - initializerERC721A - { - if (initialOwner == address(0)) { - revert InitialOwnerCannotBeZero(); - } - - __ERC721A_init_unchained(name, symbol); - __ERC721AQueryable_init_unchained(); - _initializeOwner(initialOwner); - } - - /*============================================================== - = META = - ==============================================================*/ - - /// @notice Returns the contract name and version - /// @return The contract name and version as strings - function contractNameAndVersion() public pure returns (string memory, string memory) { - return ("ERC721CMInitializable", "1.0.1"); - } - - /// @notice Gets the token URI for a specific token ID - /// @param tokenId The ID of the token - /// @return The token URI - function tokenURI(uint256 tokenId) - public - view - override(ERC721AUpgradeable, IERC721AUpgradeable) - returns (string memory) - { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _currentBaseURI; - return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; - } - - /// @notice Gets the contract URI - /// @return The contract URI - function contractURI() public view returns (string memory) { - return _contractURI; - } - - /*============================================================== - = MODIFIERS = - ==============================================================*/ - - /// @notice Modifier to check if the contract is mintable - modifier canMint() { - if (!_mintable) revert NotMintable(); - _; - } - - /// @notice Modifier to check if the total supply is enough - /// @param qty The quantity to mint - modifier hasSupply(uint256 qty) { - if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); - _; - } - - /*============================================================== - = PUBLIC WRITE METHODS = - ==============================================================*/ - - /// @notice Mints tokens for the caller - /// @param qty The quantity to mint - /// @param limit The minting limit for the caller (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) - external - payable - virtual - nonReentrant - { - _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); - } - - /// @notice Allows authorized minters to mint tokens for a specified address - /// @param to The address to mint tokens for - /// @param qty The quantity to mint - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function authorizedMint( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) external payable onlyAuthorizedMinter { - _mintInternal(qty, to, limit, proof, timestamp, signature); - } - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @notice Gets the stage info for a given stage index - /// @param index The stage index - /// @return The stage info, wallet minted count, and stage minted count - function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { - if (index >= _mintStages.length) { - revert InvalidStage(); - } - uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; - uint256 stageMinted = _stageMintedCounts[index]; - return (_mintStages[index], walletMinted, stageMinted); - } - - /// @notice Gets the mint currency address - /// @return The address of the mint currency - function getMintCurrency() external view returns (address) { - return _mintCurrency; - } - - /// @notice Gets the cosign nonce for a specific minter - /// @param minter The address of the minter - /// @return The cosign nonce - function getCosignNonce(address minter) public view returns (uint256) { - return _numberMinted(minter); - } - - /// @notice Gets the mintable status - /// @return The mintable status - function getMintable() external view returns (bool) { - return _mintable; - } - - /// @notice Gets the number of minting stages - /// @return The number of minting stages - function getNumberStages() external view override returns (uint256) { - return _mintStages.length; - } - - /// @notice Gets the maximum mintable supply - /// @return The maximum mintable supply - function getMaxMintableSupply() external view override returns (uint256) { - return _maxMintableSupply; - } - - /// @notice Gets the global wallet limit - /// @return The global wallet limit - function getGlobalWalletLimit() external view override returns (uint256) { - return _globalWalletLimit; - } - - /// @notice Gets the total minted count for a specific address - /// @param a The address to get the minted count for - /// @return The total minted count - function totalMintedByAddress(address a) external view virtual override returns (uint256) { - return _numberMinted(a); - } - - /// @notice Gets the active stage from the timestamp - /// @param timestamp The timestamp to get the active stage from - /// @return The active stage - function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { - for (uint256 i = 0; i < _mintStages.length; i++) { - if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { - return i; - } - } - revert InvalidStage(); - } - - /// @notice Checks if the contract supports a given interface - /// @param interfaceId The interface identifier - /// @return True if the contract supports the interface, false otherwise - function supportsInterface(bytes4 interfaceId) - public - view - override(ERC2981, ERC721AUpgradeable, IERC721AUpgradeable) - returns (bool) - { - return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) - || ERC721AUpgradeable.supportsInterface(interfaceId); - } - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @notice Sets up the contract with initial parameters - /// @param baseURI The base URI for the token URIs - /// @param tokenURISuffix The suffix for the token URIs - /// @param maxMintableSupply The maximum mintable supply - /// @param globalWalletLimit The global wallet limit - /// @param mintCurrency The address of the mint currency - /// @param fundReceiver The address to receive funds - /// @param initialStages The initial mint stages - /// @param royaltyReceiver The address to receive royalties - /// @param royaltyFeeNumerator The royalty fee numerator - function setup( - string calldata baseURI, - string calldata tokenURISuffix, - uint256 maxMintableSupply, - uint256 globalWalletLimit, - address mintCurrency, - address fundReceiver, - MintStageInfo[] calldata initialStages, - address royaltyReceiver, - uint96 royaltyFeeNumerator - ) external onlyOwner { - if (globalWalletLimit > maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _mintable = true; - _maxMintableSupply = maxMintableSupply; - _globalWalletLimit = globalWalletLimit; - _mintCurrency = mintCurrency; - _fundReceiver = fundReceiver; - _currentBaseURI = baseURI; - _tokenURISuffix = tokenURISuffix; - _setTimestampExpirySeconds(300); // 5 minutes - - if (initialStages.length > 0) { - _setStages(initialStages); - } - - if (royaltyReceiver != address(0)) { - setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); - } - } - - /// @notice Adds an authorized minter - /// @param minter The address to add as an authorized minter - function addAuthorizedMinter(address minter) external override onlyOwner { - _addAuthorizedMinter(minter); - } - - /// @notice Removes an authorized minter - /// @param minter The address to remove as an authorized minter - function removeAuthorizedMinter(address minter) external override onlyOwner { - _removeAuthorizedMinter(minter); - } - - /// @notice Sets the cosigner address - /// @param cosigner The address to set as the cosigner - function setCosigner(address cosigner) external override onlyOwner { - _setCosigner(cosigner); - } - - /// @notice Sets the timestamp expiry seconds - /// @param timestampExpirySeconds The expiry time in seconds for timestamps - function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { - _setTimestampExpirySeconds(timestampExpirySeconds); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function setStages(MintStageInfo[] calldata newStages) external onlyOwner { - _setStages(newStages); - } - - /// @notice Sets the mintable status - /// @param mintable The mintable status to set - function setMintable(bool mintable) external onlyOwner { - _mintable = mintable; - emit SetMintable(mintable); - } - - /// @notice Sets the default royalty for the contract - /// @param receiver The address to receive royalties - /// @param feeNumerator The royalty fee numerator - function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { - super._setDefaultRoyalty(receiver, feeNumerator); - emit DefaultRoyaltySet(receiver, feeNumerator); - } - - /// @notice Sets the maximum mintable supply - /// @param maxMintableSupply The maximum mintable supply to set - function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { - if (maxMintableSupply > _maxMintableSupply) { - revert CannotIncreaseMaxMintableSupply(); - } - _maxMintableSupply = maxMintableSupply; - emit SetMaxMintableSupply(maxMintableSupply); - } - - /// @notice Sets the global wallet limit - /// @param globalWalletLimit The global wallet limit to set - function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { - if (globalWalletLimit > _maxMintableSupply) { - revert GlobalWalletLimitOverflow(); - } - _globalWalletLimit = globalWalletLimit; - emit SetGlobalWalletLimit(globalWalletLimit); - } - - /// @notice Allows the owner to mint tokens for a specific address - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { - _safeMint(to, qty); - } - - /// @notice Withdraws the total mint fee and remaining balance from the contract - /// @dev Can only be called by the owner - function withdraw() external onlyOwner { - (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); - if (!success) revert TransferFailed(); - _totalMintFee = 0; - - uint256 remainingValue = address(this).balance; - (success,) = _fundReceiver.call{value: remainingValue}(""); - if (!success) revert WithdrawFailed(); - - emit Withdraw(_totalMintFee + remainingValue); - } - - /// @notice Withdraws ERC20 tokens from the contract - /// @dev Can only be called by the owner - function withdrawERC20() external onlyOwner { - if (_mintCurrency == address(0)) revert WrongMintCurrency(); - - uint256 totalFee = _totalMintFee; - uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); - - if (remaining < totalFee) revert InsufficientBalance(); - - _totalMintFee = 0; - uint256 totalAmount = totalFee + remaining; - - SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); - SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); - - emit WithdrawERC20(_mintCurrency, totalAmount); - } - - /// @notice Sets the base URI for the token URIs - /// @param baseURI The base URI to set - function setBaseURI(string calldata baseURI) external onlyOwner { - _currentBaseURI = baseURI; - emit SetBaseURI(baseURI); - } - - /// @notice Sets the token URI suffix - /// @param suffix The suffix to set - function setTokenURISuffix(string calldata suffix) external onlyOwner { - _tokenURISuffix = suffix; - } - - /// @notice Sets the contract URI - /// @param uri The URI to set - function setContractURI(string calldata uri) external onlyOwner { - _contractURI = uri; - emit SetContractURI(uri); - } - - /// @notice Sets the frozen status - /// @param _frozen The frozen status to set - function setFrozen(bool _frozen) external onlyOwner { - frozen = _frozen; - emit SetFrozen(_frozen); - } - - /*============================================================== - = INTERNAL HELPERS = - ==============================================================*/ - - /// @notice Internal function to handle minting logic - /// @param qty The quantity to mint - /// @param to The address to mint tokens for - /// @param limit The minting limit for the recipient (used in merkle proofs) - /// @param proof The merkle proof for allowlist minting - /// @param timestamp The timestamp for the minting action (used in cosigning) - /// @param signature The cosigner's signature - function _mintInternal( - uint32 qty, - address to, - uint32 limit, - bytes32[] calldata proof, - uint256 timestamp, - bytes calldata signature - ) internal canMint hasSupply(qty) { - uint256 stageTimestamp = block.timestamp; - bool waiveMintFee = false; - - if (getCosigner() != address(0)) { - waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); - _assertValidTimestamp(timestamp); - stageTimestamp = timestamp; - } - - uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); - MintStageInfo memory stage = _mintStages[activeStage]; - - uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; - - // Check value if minting with ETH - if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); - - // Check stage supply if applicable - if (stage.maxStageSupply > 0) { - if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { - revert StageSupplyExceeded(); - } - } - - // Check global wallet limit if applicable - if (_globalWalletLimit > 0) { - if (_numberMinted(to) + qty > _globalWalletLimit) { - revert WalletGlobalLimitExceeded(); - } - } - - // Check wallet limit for stage if applicable, limit == 0 means no limit enforced - if (stage.walletLimit > 0) { - if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { - revert WalletStageLimitExceeded(); - } - } - - // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required - if (stage.merkleRoot != 0) { - if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { - revert InvalidProof(); - } - - // Verify merkle proof mint limit - if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { - revert WalletStageLimitExceeded(); - } - } - - if (_mintCurrency != address(0)) { - // ERC20 mint payment - SafeTransferLib.safeTransferFrom( - _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty - ); - } - - _totalMintFee += adjustedMintFee * qty; - - _stageMintedCountsPerWallet[activeStage][to] += qty; - _stageMintedCounts[activeStage] += qty; - _safeMint(to, qty); - } - - /// @notice Sets the mint stages - /// @param newStages The new mint stages to set - function _setStages(MintStageInfo[] calldata newStages) internal { - delete _mintStages; - - for (uint256 i = 0; i < newStages.length;) { - if (i >= 1) { - if ( - newStages[i].startTimeUnixSeconds - < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() - ) { - revert InsufficientStageTimeGap(); - } - } - _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); - _mintStages.push( - MintStageInfo({ - price: newStages[i].price, - mintFee: newStages[i].mintFee, - walletLimit: newStages[i].walletLimit, - merkleRoot: newStages[i].merkleRoot, - maxStageSupply: newStages[i].maxStageSupply, - startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, - endTimeUnixSeconds: newStages[i].endTimeUnixSeconds - }) - ); - emit UpdateStage( - i, - newStages[i].price, - newStages[i].mintFee, - newStages[i].walletLimit, - newStages[i].merkleRoot, - newStages[i].maxStageSupply, - newStages[i].startTimeUnixSeconds, - newStages[i].endTimeUnixSeconds - ); - - unchecked { - ++i; - } - } - } - - /// @notice Blocks token transfers when the contract is frozen - /// @dev Overrides the _beforeTokenTransfers function from ERC721A - function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity) - internal - virtual - override - { - if (frozen && from != address(0)) revert TransfersAreFrozen(); - super._beforeTokenTransfers(from, to, startTokenId, quantity); - } - - /// @notice Validates the start and end timestamps for a stage - /// @param start The start timestamp - /// @param end The end timestamp - function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { - if (start >= end) revert InvalidStartAndEndTimestamp(); - } - - /// @dev Overriden to prevent double-initialization of the owner. - function _guardInitializeOwner() internal pure virtual override returns (bool) { - return true; - } -} diff --git a/test/erc721m/ERC721CMInitializableTest.t.sol b/test/erc721m/ERC721CMInitializableTest.t.sol index 212422d..2d2fb33 100644 --- a/test/erc721m/ERC721CMInitializableTest.t.sol +++ b/test/erc721m/ERC721CMInitializableTest.t.sol @@ -4,21 +4,11 @@ pragma solidity ^0.8.22; import {LibClone} from "solady/src/utils/LibClone.sol"; import {IERC721A} from "erc721a/contracts/IERC721A.sol"; import {Test} from "forge-std/Test.sol"; -import {ERC721CMInitializableV1_0_1 as ERC721CMInitializable} from - "../../contracts/nft/erc721m/ERC721CMInitializableV1_0_1.sol"; +import {ERC721CMInitializableV1_0_0 as ERC721CMInitializable} from + "../../contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol"; import {MintStageInfo} from "../../contracts/common/Structs.sol"; import {ErrorsAndEvents} from "../../contracts/common/ErrorsAndEvents.sol"; -contract MockERC721CMInitializable is ERC721CMInitializable { - function baseURI() public view returns (string memory) { - return _currentBaseURI; - } - - function tokenURISuffix() public view returns (string memory) { - return _tokenURISuffix; - } -} - contract ERC721CMInitializableTest is Test { MockERC721CMInitializable public nft; address public owner; @@ -37,12 +27,10 @@ contract ERC721CMInitializableTest is Test { vm.deal(owner, 10 ether); vm.deal(minter, 2 ether); - address clone = LibClone.deployERC1967(address(new MockERC721CMInitializable())); - nft = MockERC721CMInitializable(clone); + address clone = LibClone.deployERC1967(address(new ERC721CMInitializable())); + nft = ERC721CMInitializable(clone); nft.initialize("Test", "TEST", owner); nft.setup( - "base_uri_", - ".json", INITIAL_SUPPLY, GLOBAL_WALLET_LIMIT, address(0), @@ -187,12 +175,4 @@ contract ERC721CMInitializableTest is Test { nft.setContractURI(uri); assertEq(nft.contractURI(), uri); } - - function testBaseURISetup() public { - assertEq(nft.baseURI(), "base_uri_"); - } - - function testTokenURISuffixSetup() public { - assertEq(nft.tokenURISuffix(), ".json"); - } } diff --git a/test/erc721m/ERC721MInitializableTest.t.sol b/test/erc721m/ERC721MInitializableTest.t.sol deleted file mode 100644 index b13fffb..0000000 --- a/test/erc721m/ERC721MInitializableTest.t.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -import {console} from "forge-std/console.sol"; -import {LibClone} from "solady/src/utils/LibClone.sol"; -import {IERC721A} from "erc721a/contracts/IERC721A.sol"; -import {Test} from "forge-std/Test.sol"; -import {ERC721MInitializableV1_0_1 as ERC721MInitializable} from - "../../contracts/nft/erc721m/ERC721MInitializableV1_0_1.sol"; -import {MintStageInfo} from "../../contracts/common/Structs.sol"; -import {ErrorsAndEvents} from "../../contracts/common/ErrorsAndEvents.sol"; - -contract MockERC721M is ERC721MInitializable { - function baseURI() public view returns (string memory) { - return _currentBaseURI; - } - - function tokenURISuffix() public view returns (string memory) { - return _tokenURISuffix; - } -} - -contract ERC721MInitializableTest is Test { - MockERC721M public nft; - address public owner; - address public minter; - address public fundReceiver; - address public readonly; - uint256 public constant INITIAL_SUPPLY = 1000; - uint256 public constant GLOBAL_WALLET_LIMIT = 0; - - function setUp() public { - owner = address(this); - fundReceiver = address(0x1); - readonly = address(0x2); - minter = address(0x4); - - vm.deal(owner, 10 ether); - vm.deal(minter, 2 ether); - - address clone = LibClone.deployERC1967(address(new MockERC721M())); - nft = MockERC721M(clone); - nft.initialize("Test", "TEST", owner); - nft.setup( - "base_uri_", - ".json", - INITIAL_SUPPLY, - GLOBAL_WALLET_LIMIT, - address(0), - fundReceiver, - new MintStageInfo[](0), - address(this), - 0 - ); - } - - function testTransferWhenNotFrozen() public { - vm.startPrank(owner); - nft.setFrozen(false); - nft.ownerMint(1, minter); - vm.stopPrank(); - - vm.prank(minter); - nft.transferFrom(minter, readonly, 0); - - assertEq(nft.balanceOf(minter), 0); - assertEq(nft.balanceOf(readonly), 1); - } - - function testTransferWhenFrozen() public { - vm.startPrank(owner); - nft.setFrozen(true); - nft.ownerMint(1, minter); - vm.stopPrank(); - - vm.expectRevert(ERC721MInitializable.TransfersAreFrozen.selector); - vm.prank(minter); - nft.safeTransferFrom(minter, readonly, 0); - } - - function testBaseURISetup() public { - assertEq(nft.baseURI(), "base_uri_"); - } - - function testBaseURISuffixSetup() public { - assertEq(nft.tokenURISuffix(), ".json"); - } - - function testSetBaseURI() public { - vm.startPrank(owner); - nft.setBaseURI("new_base_uri_"); - assertEq(nft.baseURI(), "new_base_uri_"); - } - - function testSetTokenURISuffix() public { - vm.startPrank(owner); - nft.setTokenURISuffix(".txt"); - assertEq(nft.tokenURISuffix(), ".txt"); - } -} From 61a9e149e611c853d61b5fe909b37d157974a37e Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Fri, 6 Dec 2024 10:47:36 -0500 Subject: [PATCH 08/26] add old version back Signed-off-by: Adam Wolf --- .../erc721m/ERC721CMInitializableV1_0_0.sol | 539 ++++++++++++++++++ .../erc721m/ERC721MInitializableV1_0_0.sol | 539 ++++++++++++++++++ 2 files changed, 1078 insertions(+) create mode 100644 contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol create mode 100644 contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol diff --git a/contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol b/contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol new file mode 100644 index 0000000..5681d64 --- /dev/null +++ b/contracts/nft/erc721m/ERC721CMInitializableV1_0_0.sol @@ -0,0 +1,539 @@ +//SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {ERC2981} from "solady/src/tokens/ERC2981.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; +import { + ERC721ACQueryableInitializable, + ERC721AUpgradeable, + IERC721AUpgradeable +} from "../creator-token-standards/ERC721ACQueryableInitializable.sol"; +import {ERC721MStorage} from "./ERC721MStorage.sol"; +import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; +import {MintStageInfo} from "../../common/Structs.sol"; +import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; +import {Cosignable} from "../../common/Cosignable.sol"; +import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; + +/** + * @title ERC721CMInitializableV1_0_0 + * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this + * contract is for use with EIP-1167 Minimal Proxies (Clones). + */ +contract ERC721CMInitializableV1_0_0 is + IERC721MInitializable, + ERC721ACQueryableInitializable, + ERC2981, + Ownable, + ReentrancyGuard, + Cosignable, + AuthorizedMinterControl, + ERC721MStorage +{ + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract + /// @param name The name of the token collection + /// @param symbol The symbol of the token collection + /// @param initialOwner The address of the initial owner + function initialize(string calldata name, string calldata symbol, address initialOwner) + external + initializer + initializerERC721A + { + if (initialOwner == address(0)) { + revert InitialOwnerCannotBeZero(); + } + + __ERC721ACQueryableInitializable_init(name, symbol); + _initializeOwner(initialOwner); + } + + /*============================================================== + = META = + ==============================================================*/ + + /// @notice Returns the contract name and version + /// @return The contract name and version as strings + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("ERC721CMInitializable", "1.0.0"); + } + + /// @notice Gets the token URI for a specific token ID + /// @param tokenId The ID of the token + /// @return The token URI + function tokenURI(uint256 tokenId) + public + view + override(ERC721AUpgradeable, IERC721AUpgradeable) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _currentBaseURI; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; + } + + /// @notice Gets the contract URI + /// @return The contract URI + function contractURI() public view returns (string memory) { + return _contractURI; + } + + /*============================================================== + = MODIFIERS = + ==============================================================*/ + + /// @notice Modifier to check if the contract is mintable + modifier canMint() { + if (!_mintable) revert NotMintable(); + _; + } + + /// @notice Modifier to check if the total supply is enough + /// @param qty The quantity to mint + modifier hasSupply(uint256 qty) { + if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); + _; + } + + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + + /// @notice Mints tokens for the caller + /// @param qty The quantity to mint + /// @param limit The minting limit for the caller (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) + external + payable + virtual + nonReentrant + { + _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); + } + + /// @notice Allows authorized minters to mint tokens for a specified address + /// @param to The address to mint tokens for + /// @param qty The quantity to mint + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function authorizedMint( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) external payable onlyAuthorizedMinter { + _mintInternal(qty, to, limit, proof, timestamp, signature); + } + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Gets the stage info for a given stage index + /// @param index The stage index + /// @return The stage info, wallet minted count, and stage minted count + function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { + if (index >= _mintStages.length) { + revert InvalidStage(); + } + uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; + uint256 stageMinted = _stageMintedCounts[index]; + return (_mintStages[index], walletMinted, stageMinted); + } + + /// @notice Gets the mint currency address + /// @return The address of the mint currency + function getMintCurrency() external view returns (address) { + return _mintCurrency; + } + + /// @notice Gets the cosign nonce for a specific minter + /// @param minter The address of the minter + /// @return The cosign nonce + function getCosignNonce(address minter) public view returns (uint256) { + return _numberMinted(minter); + } + + /// @notice Gets the mintable status + /// @return The mintable status + function getMintable() external view returns (bool) { + return _mintable; + } + + /// @notice Gets the number of minting stages + /// @return The number of minting stages + function getNumberStages() external view override returns (uint256) { + return _mintStages.length; + } + + /// @notice Gets the maximum mintable supply + /// @return The maximum mintable supply + function getMaxMintableSupply() external view override returns (uint256) { + return _maxMintableSupply; + } + + /// @notice Gets the global wallet limit + /// @return The global wallet limit + function getGlobalWalletLimit() external view override returns (uint256) { + return _globalWalletLimit; + } + + /// @notice Gets the total minted count for a specific address + /// @param a The address to get the minted count for + /// @return The total minted count + function totalMintedByAddress(address a) external view virtual override returns (uint256) { + return _numberMinted(a); + } + + /// @notice Gets the active stage from the timestamp + /// @param timestamp The timestamp to get the active stage from + /// @return The active stage + function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { + for (uint256 i = 0; i < _mintStages.length; i++) { + if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { + return i; + } + } + revert InvalidStage(); + } + + /// @notice Checks if the contract supports a given interface + /// @param interfaceId The interface identifier + /// @return True if the contract supports the interface, false otherwise + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC2981, IERC721AUpgradeable, ERC721ACQueryableInitializable) + returns (bool) + { + return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) + || ERC721ACQueryableInitializable.supportsInterface(interfaceId); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets up the contract with initial parameters + /// @param maxMintableSupply The maximum mintable supply + /// @param globalWalletLimit The global wallet limit + /// @param mintCurrency The address of the mint currency + /// @param fundReceiver The address to receive funds + /// @param initialStages The initial mint stages + /// @param royaltyReceiver The address to receive royalties + /// @param royaltyFeeNumerator The royalty fee numerator + function setup( + uint256 maxMintableSupply, + uint256 globalWalletLimit, + address mintCurrency, + address fundReceiver, + MintStageInfo[] calldata initialStages, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) external onlyOwner { + if (globalWalletLimit > maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _mintable = true; + _maxMintableSupply = maxMintableSupply; + _globalWalletLimit = globalWalletLimit; + _mintCurrency = mintCurrency; + _fundReceiver = fundReceiver; + _setTimestampExpirySeconds(300); // 5 minutes + + if (initialStages.length > 0) { + _setStages(initialStages); + } + + if (royaltyReceiver != address(0)) { + setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + } + } + + /// @notice Adds an authorized minter + /// @param minter The address to add as an authorized minter + function addAuthorizedMinter(address minter) external override onlyOwner { + _addAuthorizedMinter(minter); + } + + /// @notice Removes an authorized minter + /// @param minter The address to remove as an authorized minter + function removeAuthorizedMinter(address minter) external override onlyOwner { + _removeAuthorizedMinter(minter); + } + + /// @notice Sets the cosigner address + /// @param cosigner The address to set as the cosigner + function setCosigner(address cosigner) external override onlyOwner { + _setCosigner(cosigner); + } + + /// @notice Sets the timestamp expiry seconds + /// @param timestampExpirySeconds The expiry time in seconds for timestamps + function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { + _setTimestampExpirySeconds(timestampExpirySeconds); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function setStages(MintStageInfo[] calldata newStages) external onlyOwner { + _setStages(newStages); + } + + /// @notice Sets the mintable status + /// @param mintable The mintable status to set + function setMintable(bool mintable) external onlyOwner { + _mintable = mintable; + emit SetMintable(mintable); + } + + /// @notice Sets the default royalty for the contract + /// @param receiver The address to receive royalties + /// @param feeNumerator The royalty fee numerator + function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { + super._setDefaultRoyalty(receiver, feeNumerator); + emit DefaultRoyaltySet(receiver, feeNumerator); + } + + /// @notice Sets the maximum mintable supply + /// @param maxMintableSupply The maximum mintable supply to set + function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { + if (maxMintableSupply > _maxMintableSupply) { + revert CannotIncreaseMaxMintableSupply(); + } + _maxMintableSupply = maxMintableSupply; + emit SetMaxMintableSupply(maxMintableSupply); + } + + /// @notice Sets the global wallet limit + /// @param globalWalletLimit The global wallet limit to set + function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { + if (globalWalletLimit > _maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _globalWalletLimit = globalWalletLimit; + emit SetGlobalWalletLimit(globalWalletLimit); + } + + /// @notice Allows the owner to mint tokens for a specific address + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { + _safeMint(to, qty); + } + + /// @notice Withdraws the total mint fee and remaining balance from the contract + /// @dev Can only be called by the owner + function withdraw() external onlyOwner { + (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); + if (!success) revert TransferFailed(); + _totalMintFee = 0; + + uint256 remainingValue = address(this).balance; + (success,) = _fundReceiver.call{value: remainingValue}(""); + if (!success) revert WithdrawFailed(); + + emit Withdraw(_totalMintFee + remainingValue); + } + + /// @notice Withdraws ERC20 tokens from the contract + /// @dev Can only be called by the owner + function withdrawERC20() external onlyOwner { + if (_mintCurrency == address(0)) revert WrongMintCurrency(); + + uint256 totalFee = _totalMintFee; + uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); + + if (remaining < totalFee) revert InsufficientBalance(); + + _totalMintFee = 0; + uint256 totalAmount = totalFee + remaining; + + SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); + SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); + + emit WithdrawERC20(_mintCurrency, totalAmount); + } + + /// @notice Sets the base URI for the token URIs + /// @param baseURI The base URI to set + function setBaseURI(string calldata baseURI) external onlyOwner { + _currentBaseURI = baseURI; + emit SetBaseURI(baseURI); + } + + /// @notice Sets the token URI suffix + /// @param suffix The suffix to set + function setTokenURISuffix(string calldata suffix) external onlyOwner { + _tokenURISuffix = suffix; + } + + /// @notice Sets the contract URI + /// @param uri The URI to set + function setContractURI(string calldata uri) external onlyOwner { + _contractURI = uri; + emit SetContractURI(uri); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Internal function to handle minting logic + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function _mintInternal( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) internal canMint hasSupply(qty) { + uint256 stageTimestamp = block.timestamp; + bool waiveMintFee = false; + + if (getCosigner() != address(0)) { + waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); + _assertValidTimestamp(timestamp); + stageTimestamp = timestamp; + } + + uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); + MintStageInfo memory stage = _mintStages[activeStage]; + + uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; + + // Check value if minting with ETH + if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); + + // Check stage supply if applicable + if (stage.maxStageSupply > 0) { + if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { + revert StageSupplyExceeded(); + } + } + + // Check global wallet limit if applicable + if (_globalWalletLimit > 0) { + if (_numberMinted(to) + qty > _globalWalletLimit) { + revert WalletGlobalLimitExceeded(); + } + } + + // Check wallet limit for stage if applicable, limit == 0 means no limit enforced + if (stage.walletLimit > 0) { + if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { + revert WalletStageLimitExceeded(); + } + } + + // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required + if (stage.merkleRoot != 0) { + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { + revert InvalidProof(); + } + + // Verify merkle proof mint limit + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + revert WalletStageLimitExceeded(); + } + } + + if (_mintCurrency != address(0)) { + // ERC20 mint payment + SafeTransferLib.safeTransferFrom( + _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty + ); + } + + _totalMintFee += adjustedMintFee * qty; + + _stageMintedCountsPerWallet[activeStage][to] += qty; + _stageMintedCounts[activeStage] += qty; + _safeMint(to, qty); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function _setStages(MintStageInfo[] calldata newStages) internal { + delete _mintStages; + + for (uint256 i = 0; i < newStages.length;) { + if (i >= 1) { + if ( + newStages[i].startTimeUnixSeconds + < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() + ) { + revert InsufficientStageTimeGap(); + } + } + _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); + _mintStages.push( + MintStageInfo({ + price: newStages[i].price, + mintFee: newStages[i].mintFee, + walletLimit: newStages[i].walletLimit, + merkleRoot: newStages[i].merkleRoot, + maxStageSupply: newStages[i].maxStageSupply, + startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, + endTimeUnixSeconds: newStages[i].endTimeUnixSeconds + }) + ); + emit UpdateStage( + i, + newStages[i].price, + newStages[i].mintFee, + newStages[i].walletLimit, + newStages[i].merkleRoot, + newStages[i].maxStageSupply, + newStages[i].startTimeUnixSeconds, + newStages[i].endTimeUnixSeconds + ); + + unchecked { + ++i; + } + } + } + + /// @notice Validates the start and end timestamps for a stage + /// @param start The start timestamp + /// @param end The end timestamp + function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { + if (start >= end) revert InvalidStartAndEndTimestamp(); + } + + /// @notice Requires the caller to be the contract owner + function _requireCallerIsContractOwner() internal view override { + return _checkOwner(); + } + + /// @dev Overriden to prevent double-initialization of the owner. + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } +} diff --git a/contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol b/contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol new file mode 100644 index 0000000..b394ba8 --- /dev/null +++ b/contracts/nft/erc721m/ERC721MInitializableV1_0_0.sol @@ -0,0 +1,539 @@ +//SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {ERC2981} from "solady/src/tokens/ERC2981.sol"; +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; +import {Initializable} from "solady/src/utils/Initializable.sol"; + +import {ERC721AUpgradeable, IERC721AUpgradeable} from "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; +import { + ERC721AQueryableUpgradeable, + IERC721AQueryableUpgradeable +} from "erc721a-upgradeable/contracts/extensions/ERC721AQueryableUpgradeable.sol"; +import {IERC721A, ERC721A} from "erc721a/contracts/extensions/ERC721AQueryable.sol"; +import {ERC721MStorage} from "./ERC721MStorage.sol"; +import {MINT_FEE_RECEIVER} from "../../utils/Constants.sol"; +import {MintStageInfo} from "../../common/Structs.sol"; +import {IERC721MInitializable} from "./interfaces/IERC721MInitializable.sol"; +import {Cosignable} from "../../common/Cosignable.sol"; +import {AuthorizedMinterControl} from "../../common/AuthorizedMinterControl.sol"; + +/** + * @title ERC721MInitializableV1_0_0 + * @dev This contract is not meant for use in Upgradeable Proxy contracts though it may base on Upgradeable contract. The purpose of this + * contract is for use with EIP-1167 Minimal Proxies (Clones). + */ +contract ERC721MInitializableV1_0_0 is + IERC721MInitializable, + ERC721AQueryableUpgradeable, + ERC2981, + Ownable, + ReentrancyGuard, + Cosignable, + AuthorizedMinterControl, + ERC721MStorage, + Initializable +{ + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the contract + /// @param name The name of the token collection + /// @param symbol The symbol of the token collection + /// @param initialOwner The address of the initial owner + function initialize(string calldata name, string calldata symbol, address initialOwner) + external + initializer + initializerERC721A + { + if (initialOwner == address(0)) { + revert InitialOwnerCannotBeZero(); + } + + __ERC721A_init_unchained(name, symbol); + __ERC721AQueryable_init_unchained(); + _initializeOwner(initialOwner); + } + + /*============================================================== + = META = + ==============================================================*/ + + /// @notice Returns the contract name and version + /// @return The contract name and version as strings + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("ERC721CMInitializable", "1.0.0"); + } + + /// @notice Gets the token URI for a specific token ID + /// @param tokenId The ID of the token + /// @return The token URI + function tokenURI(uint256 tokenId) + public + view + override(ERC721AUpgradeable, IERC721AUpgradeable) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _currentBaseURI; + return bytes(baseURI).length != 0 ? string(abi.encodePacked(baseURI, _toString(tokenId), _tokenURISuffix)) : ""; + } + + /// @notice Gets the contract URI + /// @return The contract URI + function contractURI() public view returns (string memory) { + return _contractURI; + } + + /*============================================================== + = MODIFIERS = + ==============================================================*/ + + /// @notice Modifier to check if the contract is mintable + modifier canMint() { + if (!_mintable) revert NotMintable(); + _; + } + + /// @notice Modifier to check if the total supply is enough + /// @param qty The quantity to mint + modifier hasSupply(uint256 qty) { + if (totalSupply() + qty > _maxMintableSupply) revert NoSupplyLeft(); + _; + } + + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + + /// @notice Mints tokens for the caller + /// @param qty The quantity to mint + /// @param limit The minting limit for the caller (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function mint(uint32 qty, uint32 limit, bytes32[] calldata proof, uint256 timestamp, bytes calldata signature) + external + payable + virtual + nonReentrant + { + _mintInternal(qty, msg.sender, limit, proof, timestamp, signature); + } + + /// @notice Allows authorized minters to mint tokens for a specified address + /// @param to The address to mint tokens for + /// @param qty The quantity to mint + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function authorizedMint( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) external payable onlyAuthorizedMinter { + _mintInternal(qty, to, limit, proof, timestamp, signature); + } + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Gets the stage info for a given stage index + /// @param index The stage index + /// @return The stage info, wallet minted count, and stage minted count + function getStageInfo(uint256 index) external view override returns (MintStageInfo memory, uint32, uint256) { + if (index >= _mintStages.length) { + revert InvalidStage(); + } + uint32 walletMinted = _stageMintedCountsPerWallet[index][msg.sender]; + uint256 stageMinted = _stageMintedCounts[index]; + return (_mintStages[index], walletMinted, stageMinted); + } + + /// @notice Gets the mint currency address + /// @return The address of the mint currency + function getMintCurrency() external view returns (address) { + return _mintCurrency; + } + + /// @notice Gets the cosign nonce for a specific minter + /// @param minter The address of the minter + /// @return The cosign nonce + function getCosignNonce(address minter) public view returns (uint256) { + return _numberMinted(minter); + } + + /// @notice Gets the mintable status + /// @return The mintable status + function getMintable() external view returns (bool) { + return _mintable; + } + + /// @notice Gets the number of minting stages + /// @return The number of minting stages + function getNumberStages() external view override returns (uint256) { + return _mintStages.length; + } + + /// @notice Gets the maximum mintable supply + /// @return The maximum mintable supply + function getMaxMintableSupply() external view override returns (uint256) { + return _maxMintableSupply; + } + + /// @notice Gets the global wallet limit + /// @return The global wallet limit + function getGlobalWalletLimit() external view override returns (uint256) { + return _globalWalletLimit; + } + + /// @notice Gets the total minted count for a specific address + /// @param a The address to get the minted count for + /// @return The total minted count + function totalMintedByAddress(address a) external view virtual override returns (uint256) { + return _numberMinted(a); + } + + /// @notice Gets the active stage from the timestamp + /// @param timestamp The timestamp to get the active stage from + /// @return The active stage + function getActiveStageFromTimestamp(uint256 timestamp) public view returns (uint256) { + for (uint256 i = 0; i < _mintStages.length; i++) { + if (timestamp >= _mintStages[i].startTimeUnixSeconds && timestamp < _mintStages[i].endTimeUnixSeconds) { + return i; + } + } + revert InvalidStage(); + } + + /// @notice Checks if the contract supports a given interface + /// @param interfaceId The interface identifier + /// @return True if the contract supports the interface, false otherwise + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC2981, ERC721AUpgradeable, IERC721AUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId) + || ERC721AUpgradeable.supportsInterface(interfaceId); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets up the contract with initial parameters + /// @param maxMintableSupply The maximum mintable supply + /// @param globalWalletLimit The global wallet limit + /// @param mintCurrency The address of the mint currency + /// @param fundReceiver The address to receive funds + /// @param initialStages The initial mint stages + /// @param royaltyReceiver The address to receive royalties + /// @param royaltyFeeNumerator The royalty fee numerator + function setup( + uint256 maxMintableSupply, + uint256 globalWalletLimit, + address mintCurrency, + address fundReceiver, + MintStageInfo[] calldata initialStages, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) external onlyOwner { + if (globalWalletLimit > maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _mintable = true; + _maxMintableSupply = maxMintableSupply; + _globalWalletLimit = globalWalletLimit; + _mintCurrency = mintCurrency; + _fundReceiver = fundReceiver; + _setTimestampExpirySeconds(300); // 5 minutes + + if (initialStages.length > 0) { + _setStages(initialStages); + } + + if (royaltyReceiver != address(0)) { + setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + } + } + + /// @notice Adds an authorized minter + /// @param minter The address to add as an authorized minter + function addAuthorizedMinter(address minter) external override onlyOwner { + _addAuthorizedMinter(minter); + } + + /// @notice Removes an authorized minter + /// @param minter The address to remove as an authorized minter + function removeAuthorizedMinter(address minter) external override onlyOwner { + _removeAuthorizedMinter(minter); + } + + /// @notice Sets the cosigner address + /// @param cosigner The address to set as the cosigner + function setCosigner(address cosigner) external override onlyOwner { + _setCosigner(cosigner); + } + + /// @notice Sets the timestamp expiry seconds + /// @param timestampExpirySeconds The expiry time in seconds for timestamps + function setTimestampExpirySeconds(uint256 timestampExpirySeconds) external override onlyOwner { + _setTimestampExpirySeconds(timestampExpirySeconds); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function setStages(MintStageInfo[] calldata newStages) external onlyOwner { + _setStages(newStages); + } + + /// @notice Sets the mintable status + /// @param mintable The mintable status to set + function setMintable(bool mintable) external onlyOwner { + _mintable = mintable; + emit SetMintable(mintable); + } + + /// @notice Sets the default royalty for the contract + /// @param receiver The address to receive royalties + /// @param feeNumerator The royalty fee numerator + function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner { + super._setDefaultRoyalty(receiver, feeNumerator); + emit DefaultRoyaltySet(receiver, feeNumerator); + } + + /// @notice Sets the maximum mintable supply + /// @param maxMintableSupply The maximum mintable supply to set + function setMaxMintableSupply(uint256 maxMintableSupply) external virtual onlyOwner { + if (maxMintableSupply > _maxMintableSupply) { + revert CannotIncreaseMaxMintableSupply(); + } + _maxMintableSupply = maxMintableSupply; + emit SetMaxMintableSupply(maxMintableSupply); + } + + /// @notice Sets the global wallet limit + /// @param globalWalletLimit The global wallet limit to set + function setGlobalWalletLimit(uint256 globalWalletLimit) external onlyOwner { + if (globalWalletLimit > _maxMintableSupply) { + revert GlobalWalletLimitOverflow(); + } + _globalWalletLimit = globalWalletLimit; + emit SetGlobalWalletLimit(globalWalletLimit); + } + + /// @notice Allows the owner to mint tokens for a specific address + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + function ownerMint(uint32 qty, address to) external onlyOwner hasSupply(qty) { + _safeMint(to, qty); + } + + /// @notice Withdraws the total mint fee and remaining balance from the contract + /// @dev Can only be called by the owner + function withdraw() external onlyOwner { + (bool success,) = MINT_FEE_RECEIVER.call{value: _totalMintFee}(""); + if (!success) revert TransferFailed(); + _totalMintFee = 0; + + uint256 remainingValue = address(this).balance; + (success,) = _fundReceiver.call{value: remainingValue}(""); + if (!success) revert WithdrawFailed(); + + emit Withdraw(_totalMintFee + remainingValue); + } + + /// @notice Withdraws ERC20 tokens from the contract + /// @dev Can only be called by the owner + function withdrawERC20() external onlyOwner { + if (_mintCurrency == address(0)) revert WrongMintCurrency(); + + uint256 totalFee = _totalMintFee; + uint256 remaining = SafeTransferLib.balanceOf(_mintCurrency, address(this)); + + if (remaining < totalFee) revert InsufficientBalance(); + + _totalMintFee = 0; + uint256 totalAmount = totalFee + remaining; + + SafeTransferLib.safeTransfer(_mintCurrency, MINT_FEE_RECEIVER, totalFee); + SafeTransferLib.safeTransfer(_mintCurrency, _fundReceiver, remaining); + + emit WithdrawERC20(_mintCurrency, totalAmount); + } + + /// @notice Sets the base URI for the token URIs + /// @param baseURI The base URI to set + function setBaseURI(string calldata baseURI) external onlyOwner { + _currentBaseURI = baseURI; + emit SetBaseURI(baseURI); + } + + /// @notice Sets the token URI suffix + /// @param suffix The suffix to set + function setTokenURISuffix(string calldata suffix) external onlyOwner { + _tokenURISuffix = suffix; + } + + /// @notice Sets the contract URI + /// @param uri The URI to set + function setContractURI(string calldata uri) external onlyOwner { + _contractURI = uri; + emit SetContractURI(uri); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Internal function to handle minting logic + /// @param qty The quantity to mint + /// @param to The address to mint tokens for + /// @param limit The minting limit for the recipient (used in merkle proofs) + /// @param proof The merkle proof for allowlist minting + /// @param timestamp The timestamp for the minting action (used in cosigning) + /// @param signature The cosigner's signature + function _mintInternal( + uint32 qty, + address to, + uint32 limit, + bytes32[] calldata proof, + uint256 timestamp, + bytes calldata signature + ) internal canMint hasSupply(qty) { + uint256 stageTimestamp = block.timestamp; + bool waiveMintFee = false; + + if (getCosigner() != address(0)) { + waiveMintFee = assertValidCosign(msg.sender, qty, timestamp, signature, getCosignNonce(msg.sender)); + _assertValidTimestamp(timestamp); + stageTimestamp = timestamp; + } + + uint256 activeStage = getActiveStageFromTimestamp(stageTimestamp); + MintStageInfo memory stage = _mintStages[activeStage]; + + uint80 adjustedMintFee = waiveMintFee ? 0 : stage.mintFee; + + // Check value if minting with ETH + if (_mintCurrency == address(0) && msg.value < (stage.price + adjustedMintFee) * qty) revert NotEnoughValue(); + + // Check stage supply if applicable + if (stage.maxStageSupply > 0) { + if (_stageMintedCounts[activeStage] + qty > stage.maxStageSupply) { + revert StageSupplyExceeded(); + } + } + + // Check global wallet limit if applicable + if (_globalWalletLimit > 0) { + if (_numberMinted(to) + qty > _globalWalletLimit) { + revert WalletGlobalLimitExceeded(); + } + } + + // Check wallet limit for stage if applicable, limit == 0 means no limit enforced + if (stage.walletLimit > 0) { + if (_stageMintedCountsPerWallet[activeStage][to] + qty > stage.walletLimit) { + revert WalletStageLimitExceeded(); + } + } + + // Check merkle proof if applicable, merkleRoot == 0x00...00 means no proof required + if (stage.merkleRoot != 0) { + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to, limit)))) { + revert InvalidProof(); + } + + // Verify merkle proof mint limit + if (limit > 0 && _stageMintedCountsPerWallet[activeStage][to] + qty > limit) { + revert WalletStageLimitExceeded(); + } + } + + if (_mintCurrency != address(0)) { + // ERC20 mint payment + SafeTransferLib.safeTransferFrom( + _mintCurrency, msg.sender, address(this), (stage.price + adjustedMintFee) * qty + ); + } + + _totalMintFee += adjustedMintFee * qty; + + _stageMintedCountsPerWallet[activeStage][to] += qty; + _stageMintedCounts[activeStage] += qty; + _safeMint(to, qty); + } + + /// @notice Sets the mint stages + /// @param newStages The new mint stages to set + function _setStages(MintStageInfo[] calldata newStages) internal { + delete _mintStages; + + for (uint256 i = 0; i < newStages.length;) { + if (i >= 1) { + if ( + newStages[i].startTimeUnixSeconds + < newStages[i - 1].endTimeUnixSeconds + getTimestampExpirySeconds() + ) { + revert InsufficientStageTimeGap(); + } + } + _assertValidStartAndEndTimestamp(newStages[i].startTimeUnixSeconds, newStages[i].endTimeUnixSeconds); + _mintStages.push( + MintStageInfo({ + price: newStages[i].price, + mintFee: newStages[i].mintFee, + walletLimit: newStages[i].walletLimit, + merkleRoot: newStages[i].merkleRoot, + maxStageSupply: newStages[i].maxStageSupply, + startTimeUnixSeconds: newStages[i].startTimeUnixSeconds, + endTimeUnixSeconds: newStages[i].endTimeUnixSeconds + }) + ); + emit UpdateStage( + i, + newStages[i].price, + newStages[i].mintFee, + newStages[i].walletLimit, + newStages[i].merkleRoot, + newStages[i].maxStageSupply, + newStages[i].startTimeUnixSeconds, + newStages[i].endTimeUnixSeconds + ); + + unchecked { + ++i; + } + } + } + + /// @notice Validates the start and end timestamps for a stage + /// @param start The start timestamp + /// @param end The end timestamp + function _assertValidStartAndEndTimestamp(uint256 start, uint256 end) internal pure { + if (start >= end) revert InvalidStartAndEndTimestamp(); + } + + /// @dev Overriden to prevent double-initialization of the owner. + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } +} From 6dd656b2fc7aeda188cde459aa54970d3d735acd Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Fri, 6 Dec 2024 10:54:53 -0500 Subject: [PATCH 09/26] fix Signed-off-by: Adam Wolf --- test/erc721m/ERC721CMInitializableTest.t.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/erc721m/ERC721CMInitializableTest.t.sol b/test/erc721m/ERC721CMInitializableTest.t.sol index 2d2fb33..a9f35c5 100644 --- a/test/erc721m/ERC721CMInitializableTest.t.sol +++ b/test/erc721m/ERC721CMInitializableTest.t.sol @@ -10,7 +10,7 @@ import {MintStageInfo} from "../../contracts/common/Structs.sol"; import {ErrorsAndEvents} from "../../contracts/common/ErrorsAndEvents.sol"; contract ERC721CMInitializableTest is Test { - MockERC721CMInitializable public nft; + ERC721CMInitializable public nft; address public owner; address public minter; address public fundReceiver; @@ -31,13 +31,7 @@ contract ERC721CMInitializableTest is Test { nft = ERC721CMInitializable(clone); nft.initialize("Test", "TEST", owner); nft.setup( - INITIAL_SUPPLY, - GLOBAL_WALLET_LIMIT, - address(0), - fundReceiver, - new MintStageInfo[](0), - address(this), - 0 + INITIAL_SUPPLY, GLOBAL_WALLET_LIMIT, address(0), fundReceiver, new MintStageInfo[](0), address(this), 0 ); } From b5e3f9f64f07e942ff335c91c24e1216d6fa748d Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 9 Dec 2024 18:00:20 -0500 Subject: [PATCH 10/26] add docs Signed-off-by: Adam Wolf --- .../ERC721AConduitPreapprovedCloneable.sol | 13 +- .../clones/ERC721MagicDropCloneable.sol | 284 ++++++++++++------ contracts/nft/erc721m/clones/Types.sol | 67 +++++ 3 files changed, 259 insertions(+), 105 deletions(-) create mode 100644 contracts/nft/erc721m/clones/Types.sol diff --git a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol index 573dcac..418bbeb 100644 --- a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol @@ -5,18 +5,14 @@ import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {IERC721A} from "erc721a/contracts/IERC721A.sol"; -/** - * @title ERC721AConduitPreapprovedCloneable - * @notice ERC721A with the MagicEden conduit preapproved. - */ +/// @title ERC721AConduitPreapprovedCloneable +/// @notice ERC721A with the MagicEden conduit preapproved. abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneable { /// @dev The canonical MagicEden conduit. address internal constant _CONDUIT = 0x2052f8A2Ff46283B30084e5d84c89A2fdBE7f74b; - /** - * @dev Returns if the `operator` is allowed to manage all of the - * assets of `owner`. Always returns true for the conduit. - */ + /// @dev Returns if the `operator` is allowed to manage all of the + /// assets of `owner`. Always returns true for the conduit. function isApprovedForAll(address owner, address operator) public view @@ -27,6 +23,7 @@ abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneabl if (operator == _CONDUIT) { return true; } + return ERC721ACloneable.isApprovedForAll(owner, operator); } } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 1e58e1e..f595e4f 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -10,52 +10,76 @@ import {IERC721A} from "erc721a/contracts/IERC721A.sol"; import {ERC721MagicDropMetadataCloneable} from "./ERC721MagicDropMetadataCloneable.sol"; import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; +import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; + +/// @title ERC721MagicDropCloneable +/// @notice ERC721A with Public and Allowlist minting stages. +/// @dev This contract is cloneable and provides minting functionality with public and allowlist stages. contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, ReentrancyGuard { + + /*============================================================== + = STORAGE = + ==============================================================*/ + + /// @dev The recipient of the mint proceeds. address private _payoutRecipient; - address private _feeRecipient; - uint256 private _mintFee; - uint256 private _totalMintFee; + /// @dev The recipient of extra mint fees. + address public constant PROTOCOL_FEE_RECIPIENT = 0x0000000000000000000000000000000000000000; + + /// @dev The mint fee in basis points (bps). + uint256 public constant PROTOCOL_FEE_BPS = 500; // 5% + + /// @dev The denominator for basis points (bps). + uint256 public constant BPS_DENOMINATOR = 10_000; + + /// @dev Storage for the public mint stage. PublicStage private _publicStage; + + /// @dev Storage for the allowlist mint stage. AllowlistStage private _allowlistStage; - struct PublicStage { - uint256 startTime; - uint256 endTime; - uint256 price; - } + /*============================================================== + = EVENTS = + ==============================================================*/ - struct AllowlistStage { - uint256 startTime; - uint256 endTime; - uint256 price; - bytes32 merkleRoot; - } + /// @notice Emitted when the token is deployed. + event MagicDropTokenDeployed(); - struct SetupConfig { - uint256 maxSupply; - uint256 walletLimit; - string baseURI; - string contractURI; - PublicStage publicStage; - AllowlistStage allowlistStage; - address payoutRecipient; - address feeRecipient; - uint256 mintFee; - bytes32 provenanceHash; - } + /// @notice Emitted when the funds are withdrawn. + event Withdraw(uint256 amount); + /*============================================================== + = ERRORS = + ==============================================================*/ + + /// @notice Thrown when the public mint stage is not active. error PublicStageNotActive(); + + /// @notice Thrown when the allowlist mint stage is not active. error AllowlistStageNotActive(); + + /// @notice Thrown when the user does not send enough value. error NotEnoughValue(); + + /// @notice Thrown when the wallet limit is exceeded. error WalletLimitExceeded(); + + /// @notice Thrown when the withdraw fails. error WithdrawFailed(); + + /// @notice Thrown when the proof is invalid. error InvalidProof(); - event MagicDropTokenDeployed(); - event Withdraw(uint256 amount); + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + /// @notice Initializes the contract. + /// @param _name The name of the token. + /// @param _symbol The symbol of the token. + /// @param _owner The owner of the contract. function initialize(string memory _name, string memory _symbol, address _owner) public initializer { __ERC721ACloneable__init(_name, _symbol); _initializeOwner(_owner); @@ -63,59 +87,20 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc emit MagicDropTokenDeployed(); } - function burn(uint256 tokenId) external { - _burn(tokenId, true); - } - - function _startTokenId() internal view virtual override returns (uint256) { - return 1; - } - - function tokenURI(uint256 tokenId) - public - view - virtual - override(ERC721ACloneable, IERC721A) - returns (string memory) - { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _baseURI(); - bool isBaseURIEmpty = bytes(baseURI).length == 0; - bool hasNoTrailingSlash = bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]; - - if (isBaseURIEmpty) { - return ""; - } - - if (hasNoTrailingSlash) { - return baseURI; - } - - return string(abi.encodePacked(baseURI, _toString(tokenId))); - } - - function setPublicStage(PublicStage calldata stage) external onlyOwner { - _publicStage = stage; - } - - function setAllowlistStage(AllowlistStage calldata stage) external onlyOwner { - _allowlistStage = stage; - } - - function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { - _payoutRecipient = newPayoutRecipient; - } + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + /// @notice Mints tokens to the specified address. + /// @param qty The quantity of tokens to mint. + /// @param to The address to mint the tokens to. function mintPublic(uint256 qty, address to) external payable { PublicStage memory stage = _publicStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); } - uint256 mintFees = _mintFee * qty; - - if (msg.value < stage.price * qty + mintFees) { + if (msg.value < stage.price * qty) { revert NotEnoughValue(); } @@ -123,11 +108,13 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert WalletLimitExceeded(); } - _totalMintFee += mintFees; - _safeMint(to, qty); } + /// @notice Mints tokens to the specified address. + /// @param qty The quantity of tokens to mint. + /// @param to The address to mint the tokens to. + /// @param proof The Merkle proof for the allowlist mint stage. function mintAllowlist(uint256 qty, address to, bytes32[] calldata proof) external payable { AllowlistStage memory stage = _allowlistStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { @@ -138,9 +125,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert InvalidProof(); } - uint256 mintFees = _mintFee * qty; - - if (msg.value < stage.price * qty + mintFees) { + if (msg.value < stage.price * qty) { revert NotEnoughValue(); } @@ -148,20 +133,69 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert WalletLimitExceeded(); } - _totalMintFee += mintFees; - _safeMint(to, qty); } - function mintFee() external view returns (uint256) { - return _mintFee; + /// @notice Burns a token. + /// @param tokenId The token ID to burn. + function burn(uint256 tokenId) external { + _burn(tokenId, true); } + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Gets the public mint stage. + /// @return The public mint stage. + function getPublicStage() external view returns (PublicStage memory) { + return _publicStage; + } + + /// @notice Gets the allowlist mint stage. + /// @return The allowlist mint stage. + function getAllowlistStage() external view returns (AllowlistStage memory) { + return _allowlistStage; + } + + /// @notice Gets the payout recipient. + /// @return The payout recipient. + function payoutRecipient() external view returns (address) { + return _payoutRecipient; + } + + /// @notice Gets the fee recipient. + /// @return The fee recipient. + function feeRecipient() external view returns (address) { + return _feeRecipient; + } + + /// @notice Supports the ERC721MagicDropMetadata interface. + /// @param interfaceId The interface ID. + /// @return True if the interface is supported, false otherwise. + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721MagicDropMetadataCloneable) + returns (bool) + { + return interfaceId == type(IERC721MagicDropMetadata).interfaceId || super.supportsInterface(interfaceId); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Configures the contract with the provided setup parameters. + /// @param config The configuration parameters for setting up the contract. function setup(SetupConfig calldata config) external onlyOwner { if (config.maxSupply > 0) { this.setMaxSupply(config.maxSupply); } + // A wallet limit of 0 means unlimited mints per wallet + // Otherwise, wallets can only mint up to the specified limit if (config.walletLimit > 0) { this.setWalletLimit(config.walletLimit); } @@ -195,32 +229,88 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc } } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(ERC721MagicDropMetadataCloneable) - returns (bool) - { - return interfaceId == type(IERC721MagicDropMetadata).interfaceId || super.supportsInterface(interfaceId); + /// @notice Sets the public mint stage. + /// @param stage The configuration for the public mint stage. + function setPublicStage(PublicStage calldata stage) external onlyOwner { + _publicStage = stage; + } + + /// @notice Sets the allowlist mint stage. + /// @param stage The configuration for the allowlist mint stage. + function setAllowlistStage(AllowlistStage calldata stage) external onlyOwner { + _allowlistStage = stage; + } + + /// @notice Sets the payout recipient. + /// @param newPayoutRecipient The address to receive the payout from mint proceeds. + function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { + _payoutRecipient = newPayoutRecipient; } /// @notice Withdraws the total mint fee and remaining balance from the contract /// @dev Can only be called by the owner function withdraw() external onlyOwner { - (bool success,) = _feeRecipient.call{value: _totalMintFee}(""); - if (!success) revert WithdrawFailed(); - _totalMintFee = 0; + uint256 balance = address(this).balance; + uint256 protocolFee = (balance * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; + uint256 remainingBalance = balance - protocolFee; + + // Transfer protocol fee + (bool feeSuccess,) = PROTOCOL_FEE_RECIPIENT.call{value: protocolFee}(""); + if (!feeSuccess) revert WithdrawFailed(); - uint256 remainingValue = address(this).balance; - (success,) = _payoutRecipient.call{value: remainingValue}(""); + // Transfer remaining balance to the payout recipient + (bool success,) = _payoutRecipient.call{value: remainingBalance}(""); if (!success) revert WithdrawFailed(); - emit Withdraw(_totalMintFee + remainingValue); + emit Withdraw(balance); + } + + /*============================================================== + = META = + ==============================================================*/ + + /// @notice Returns the contract name and version + /// @return The contract name and version as strings + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("ERC721MagicDropCloneable", "1.0.0"); + } + + + /// @notice Gets the token URI for a given token ID. + /// @param tokenId The token ID. + /// @return The token URI. + function tokenURI(uint256 tokenId) + public + view + virtual + override(ERC721ACloneable, IERC721A) + returns (string memory) + { + if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); + + string memory baseURI = _baseURI(); + bool isBaseURIEmpty = bytes(baseURI).length == 0; + bool hasNoTrailingSlash = bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]; + + if (isBaseURIEmpty) { + return ""; + } + + if (hasNoTrailingSlash) { + return baseURI; + } + + return string(abi.encodePacked(baseURI, _toString(tokenId))); } /// @dev Overriden to prevent double-initialization of the owner. function _guardInitializeOwner() internal pure virtual override returns (bool) { return true; } + + /// @notice Gets the starting token ID. + /// @return The starting token ID. + function _startTokenId() internal view virtual override returns (uint256) { + return 1; + } } diff --git a/contracts/nft/erc721m/clones/Types.sol b/contracts/nft/erc721m/clones/Types.sol new file mode 100644 index 0000000..ae3b0c8 --- /dev/null +++ b/contracts/nft/erc721m/clones/Types.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +struct PublicStage { + /// @dev The start time of the public mint stage. + uint256 startTime; + + /// @dev The end time of the public mint stage. + uint256 endTime; + + /// @dev The price of the public mint stage. + uint256 price; +} + +struct AllowlistStage { + /// @dev The start time of the allowlist mint stage. + uint256 startTime; + + /// @dev The end time of the allowlist mint stage. + uint256 endTime; + + /// @dev The price of the allowlist mint stage. + uint256 price; + + /// @dev The merkle root of the allowlist. + bytes32 merkleRoot; +} + +struct SetupConfig { + /// @dev The maximum number of tokens that can be minted. + /// - Can be decreased if current supply < new max supply + /// - Cannot be increased once set + uint256 maxSupply; + + /// @dev The maximum number of tokens that can be minted per wallet + /// @notice A value of 0 indicates unlimited mints per wallet + uint256 walletLimit; + + /// @dev The base URI of the token. + string baseURI; + + /// @dev The contract URI of the token. + string contractURI; + + /// @dev The public mint stage. + PublicStage publicStage; + + /// @dev The allowlist mint stage. + AllowlistStage allowlistStage; + + /// @dev The payout recipient of the token. + address payoutRecipient; + + /// @dev The recipient of mint fees. + /// @notice Mint fees are sent when funds are withdrawn. + address feeRecipient; + + /// @dev The mint fee. + /// @notice This is the fee charged for each mint. + uint256 mintFee; + + /// @dev The provenance hash of the token. + /// @notice This is used to ensure the metadata is not tampered with. + /// A value of 0 is used to indicate that the provenance hash is not set. + bytes32 provenanceHash; +} \ No newline at end of file From 2eff6756d1c4d6daf610d2fb87881b9a4b675766 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 9 Dec 2024 18:17:25 -0500 Subject: [PATCH 11/26] stage logic Signed-off-by: Adam Wolf --- .../ERC721AConduitPreapprovedCloneable.sol | 2 +- .../clones/ERC721MagicDropCloneable.sol | 48 ++++++++++++++----- contracts/nft/erc721m/clones/Types.sol | 18 +------ 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol index 418bbeb..5fa7f9b 100644 --- a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol @@ -23,7 +23,7 @@ abstract contract ERC721AConduitPreapprovedCloneable is ERC721AQueryableCloneabl if (operator == _CONDUIT) { return true; } - + return ERC721ACloneable.isApprovedForAll(owner, operator); } } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index f595e4f..227f1cf 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -12,12 +12,10 @@ import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; - /// @title ERC721MagicDropCloneable /// @notice ERC721A with Public and Allowlist minting stages. /// @dev This contract is cloneable and provides minting functionality with public and allowlist stages. contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, ReentrancyGuard { - /*============================================================== = STORAGE = ==============================================================*/ @@ -72,6 +70,12 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// @notice Thrown when the proof is invalid. error InvalidProof(); + /// @notice Thrown when the stage time is invalid. + error InvalidStageTime(); + + /// @notice Thrown when the allowlist stage is invalid. + error InvalidAllowlistStageTime(); + /*============================================================== = INITIALIZERS = ==============================================================*/ @@ -164,12 +168,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc return _payoutRecipient; } - /// @notice Gets the fee recipient. - /// @return The fee recipient. - function feeRecipient() external view returns (address) { - return _feeRecipient; - } - /// @notice Supports the ERC721MagicDropMetadata interface. /// @param interfaceId The interface ID. /// @return True if the interface is supported, false otherwise. @@ -220,24 +218,45 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc this.setPayoutRecipient(config.payoutRecipient); } - if (config.feeRecipient != address(0)) { - _feeRecipient = config.feeRecipient; - } - if (config.provenanceHash != bytes32(0)) { this.setProvenanceHash(config.provenanceHash); } } /// @notice Sets the public mint stage. + /// @dev The public stage must end before the allowlist stage begins. /// @param stage The configuration for the public mint stage. function setPublicStage(PublicStage calldata stage) external onlyOwner { + if (stage.startTime >= stage.endTime) { + revert InvalidStageTime(); + } + + // If the allowlist stage is set, ensure the public stage ends before the allowlist stage begins + if (_allowlistStage.startTime != 0 && _allowlistStage.endTime != 0) { + if (stage.endTime > _allowlistStage.startTime) { + revert InvalidAllowlistStageTime(); + } + } + _publicStage = stage; } /// @notice Sets the allowlist mint stage. + /// @dev The allowlist stage must end before the public stage begins. /// @param stage The configuration for the allowlist mint stage. function setAllowlistStage(AllowlistStage calldata stage) external onlyOwner { + // Validate that the start time is before the end time + if (stage.startTime >= stage.endTime) { + revert InvalidStageTime(); + } + + // If the public stage is set, ensure the allowlist stage ends before the public stage begins + if (_publicStage.startTime != 0 && _publicStage.endTime != 0) { + if (stage.endTime > _publicStage.startTime) { + revert InvalidAllowlistStageTime(); + } + } + _allowlistStage = stage; } @@ -275,7 +294,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc return ("ERC721MagicDropCloneable", "1.0.0"); } - /// @notice Gets the token URI for a given token ID. /// @param tokenId The token ID. /// @return The token URI. @@ -303,6 +321,10 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc return string(abi.encodePacked(baseURI, _toString(tokenId))); } + /*============================================================== + = MISC = + ==============================================================*/ + /// @dev Overriden to prevent double-initialization of the owner. function _guardInitializeOwner() internal pure virtual override returns (bool) { return true; diff --git a/contracts/nft/erc721m/clones/Types.sol b/contracts/nft/erc721m/clones/Types.sol index ae3b0c8..5f7a5e2 100644 --- a/contracts/nft/erc721m/clones/Types.sol +++ b/contracts/nft/erc721m/clones/Types.sol @@ -5,24 +5,19 @@ pragma solidity ^0.8.0; struct PublicStage { /// @dev The start time of the public mint stage. uint256 startTime; - /// @dev The end time of the public mint stage. uint256 endTime; - /// @dev The price of the public mint stage. uint256 price; } - + struct AllowlistStage { /// @dev The start time of the allowlist mint stage. uint256 startTime; - /// @dev The end time of the allowlist mint stage. uint256 endTime; - /// @dev The price of the allowlist mint stage. uint256 price; - /// @dev The merkle root of the allowlist. bytes32 merkleRoot; } @@ -32,36 +27,27 @@ struct SetupConfig { /// - Can be decreased if current supply < new max supply /// - Cannot be increased once set uint256 maxSupply; - /// @dev The maximum number of tokens that can be minted per wallet /// @notice A value of 0 indicates unlimited mints per wallet uint256 walletLimit; - /// @dev The base URI of the token. string baseURI; - /// @dev The contract URI of the token. string contractURI; - /// @dev The public mint stage. PublicStage publicStage; - /// @dev The allowlist mint stage. AllowlistStage allowlistStage; - /// @dev The payout recipient of the token. address payoutRecipient; - /// @dev The recipient of mint fees. /// @notice Mint fees are sent when funds are withdrawn. address feeRecipient; - /// @dev The mint fee. /// @notice This is the fee charged for each mint. uint256 mintFee; - /// @dev The provenance hash of the token. /// @notice This is used to ensure the metadata is not tampered with. /// A value of 0 is used to indicate that the provenance hash is not set. bytes32 provenanceHash; -} \ No newline at end of file +} From 8d9bd490b1c23b694dabed0f18a0e9c19e45ca18 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 9 Dec 2024 20:54:54 -0500 Subject: [PATCH 12/26] docs and tests Signed-off-by: Adam Wolf --- .../nft/erc721m/clones/ERC721ACloneable.sol | 48 +- .../clones/ERC721AQueryableCloneable.sol | 12 +- .../clones/ERC721MagicDropCloneable.sol | 241 ++++++---- .../ERC721MagicDropMetadataCloneable.sol | 199 +++++--- contracts/nft/erc721m/clones/Types.sol | 6 - foundry.toml | 1 + .../clones/ERC721MagicDropCloneable.t.sol | 433 ++++++++++++++++++ 7 files changed, 747 insertions(+), 193 deletions(-) create mode 100644 test/erc721m/clones/ERC721MagicDropCloneable.t.sol diff --git a/contracts/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol index cb18bda..401fde7 100644 --- a/contracts/nft/erc721m/clones/ERC721ACloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721ACloneable.sol @@ -148,7 +148,9 @@ contract ERC721ACloneable is IERC721A, Initializable { _symbol = symbol_; _currentIndex = _startTokenId(); - if (_sequentialUpTo() < _startTokenId()) _revert(SequentialUpToTooSmall.selector); + if (_sequentialUpTo() < _startTokenId()) { + _revert(SequentialUpToTooSmall.selector); + } } // ============================================================= @@ -395,7 +397,9 @@ contract ERC721ACloneable is IERC721A, Initializable { // If the data at the starting slot does not exist, start the scan. if (packed == 0) { - if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); + if (tokenId >= _currentIndex) { + _revert(OwnerQueryForNonexistentToken.selector); + } // Invariant: // There will always be an initialized ownership slot // (i.e. `ownership.addr != address(0) && ownership.burned == false`) @@ -482,7 +486,9 @@ contract ERC721ACloneable is IERC721A, Initializable { * - `tokenId` must exist. */ function getApproved(uint256 tokenId) public view virtual override returns (address) { - if (!_exists(tokenId)) _revert(ApprovalQueryForNonexistentToken.selector); + if (!_exists(tokenId)) { + _revert(ApprovalQueryForNonexistentToken.selector); + } return _tokenApprovals[tokenId].value; } @@ -521,7 +527,9 @@ contract ERC721ACloneable is IERC721A, Initializable { */ function _exists(uint256 tokenId) internal view virtual returns (bool result) { if (_startTokenId() <= tokenId) { - if (tokenId > _sequentialUpTo()) return _packedOwnershipExists(_packedOwnerships[tokenId]); + if (tokenId > _sequentialUpTo()) { + return _packedOwnershipExists(_packedOwnerships[tokenId]); + } if (tokenId < _currentIndex) { uint256 packed; @@ -599,13 +607,17 @@ contract ERC721ACloneable is IERC721A, Initializable { // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); - if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); + if (address(uint160(prevOwnershipPacked)) != from) { + _revert(TransferFromIncorrectOwner.selector); + } (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) { - if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + if (!isApprovedForAll(from, _msgSenderERC721A())) { + _revert(TransferCallerNotOwnerNorApproved.selector); + } } _beforeTokenTransfers(from, to, tokenId, 1); @@ -813,7 +825,9 @@ contract ERC721ACloneable is IERC721A, Initializable { uint256 end = startTokenId + quantity; uint256 tokenId = startTokenId; - if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); + if (end - 1 > _sequentialUpTo()) { + _revert(SequentialMintExceedsLimit.selector); + } do { assembly { @@ -861,7 +875,9 @@ contract ERC721ACloneable is IERC721A, Initializable { uint256 startTokenId = _currentIndex; if (to == address(0)) _revert(MintToZeroAddress.selector); if (quantity == 0) _revert(MintZeroQuantity.selector); - if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector); + if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) { + _revert(MintERC2309QuantityExceedsLimit.selector); + } _beforeTokenTransfers(address(0), to, startTokenId, quantity); @@ -882,7 +898,9 @@ contract ERC721ACloneable is IERC721A, Initializable { _packedOwnerships[startTokenId] = _packOwnershipData(to, _nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)); - if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector); + if (startTokenId + quantity - 1 > _sequentialUpTo()) { + _revert(SequentialMintExceedsLimit.selector); + } emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to); @@ -944,9 +962,13 @@ contract ERC721ACloneable is IERC721A, Initializable { * Emits a {Transfer} event for each mint. */ function _mintSpot(address to, uint256 tokenId) internal virtual { - if (tokenId <= _sequentialUpTo()) _revert(SpotMintTokenIdTooSmall.selector); + if (tokenId <= _sequentialUpTo()) { + _revert(SpotMintTokenIdTooSmall.selector); + } uint256 prevOwnershipPacked = _packedOwnerships[tokenId]; - if (_packedOwnershipExists(prevOwnershipPacked)) _revert(TokenAlreadyExists.selector); + if (_packedOwnershipExists(prevOwnershipPacked)) { + _revert(TokenAlreadyExists.selector); + } _beforeTokenTransfers(address(0), to, tokenId, 1); @@ -1098,7 +1120,9 @@ contract ERC721ACloneable is IERC721A, Initializable { if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) { - if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); + if (!isApprovedForAll(from, _msgSenderERC721A())) { + _revert(TransferCallerNotOwnerNorApproved.selector); + } } } diff --git a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol index 82a0f2b..194af04 100644 --- a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol @@ -130,7 +130,9 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab */ function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { // If spot mints are enabled, full-range scan is disabled. - if (_sequentialUpTo() != type(uint256).max) _revert(NotCompatibleWithSpotMints.selector); + if (_sequentialUpTo() != type(uint256).max) { + _revert(NotCompatibleWithSpotMints.selector); + } uint256 start = _startTokenId(); uint256 stop = _nextTokenId(); uint256[] memory tokenIds; @@ -165,7 +167,9 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab // If there are one or more tokens to scan. if (tokenIdsMaxLength != 0) { // Set `tokenIdsMaxLength = min(balanceOf(owner), tokenIdsMaxLength)`. - if (stop - start <= tokenIdsMaxLength) tokenIdsMaxLength = stop - start; + if (stop - start <= tokenIdsMaxLength) { + tokenIdsMaxLength = stop - start; + } uint256 m; // Start of available memory. assembly { // Grab the free memory pointer. @@ -192,7 +196,9 @@ abstract contract ERC721AQueryableCloneable is ERC721ACloneable, IERC721AQueryab // Skip the remaining unused sequential slots. if (start == nextTokenId) start = _sequentialUpTo() + 1; // Reset `currOwnershipAddr`, as each spot-minted token is a batch of one. - if (start > _sequentialUpTo()) currOwnershipAddr = address(0); + if (start > _sequentialUpTo()) { + currOwnershipAddr = address(0); + } } ownership = _ownershipAt(start); // This implicitly allocates memory. assembly { diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 227f1cf..c2e31a7 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -13,80 +13,92 @@ import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.s import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; /// @title ERC721MagicDropCloneable -/// @notice ERC721A with Public and Allowlist minting stages. -/// @dev This contract is cloneable and provides minting functionality with public and allowlist stages. +/// @notice A cloneable ERC-721A drop contract that supports both a public minting stage and an allowlist minting stage. +/// @dev This contract extends metadata configuration, ownership, and royalty support from its parent, while adding +/// time-gated, price-defined minting stages. It also incorporates a payout recipient and protocol fee structure. contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, ReentrancyGuard { /*============================================================== = STORAGE = ==============================================================*/ - /// @dev The recipient of the mint proceeds. + /// @dev Address that receives the primary sale proceeds of minted tokens. + /// Configurable by the owner. If unset, withdrawals may fail. address private _payoutRecipient; - /// @dev The recipient of extra mint fees. - address public constant PROTOCOL_FEE_RECIPIENT = 0x0000000000000000000000000000000000000000; + /// @dev The address that receives protocol fees on withdrawal. + /// @notice This is fixed and cannot be changed. + address public constant PROTOCOL_FEE_RECIPIENT = 0xA3833016a4eC61f5c253D71c77522cC8A1cC1106; - /// @dev The mint fee in basis points (bps). + /// @dev The protocol fee expressed in basis points (e.g., 500 = 5%). + /// @notice This fee is taken from the contract's entire balance upon withdrawal. uint256 public constant PROTOCOL_FEE_BPS = 500; // 5% - /// @dev The denominator for basis points (bps). + /// @dev The denominator used for calculating basis points. + /// @notice 10,000 BPS = 100%. A fee of 500 BPS is therefore 5%. uint256 public constant BPS_DENOMINATOR = 10_000; - /// @dev Storage for the public mint stage. + /// @dev Configuration of the public mint stage, including timing and price. + /// @notice Public mints occur only if the current timestamp is within [startTime, endTime]. PublicStage private _publicStage; - /// @dev Storage for the allowlist mint stage. + /// @dev Configuration of the allowlist mint stage, including timing, price, and a merkle root for verification. + /// @notice Only addresses proven by a valid Merkle proof can mint during this stage. AllowlistStage private _allowlistStage; /*============================================================== = EVENTS = ==============================================================*/ - /// @notice Emitted when the token is deployed. + /// @notice Emitted once when the token contract is deployed and initialized. event MagicDropTokenDeployed(); - /// @notice Emitted when the funds are withdrawn. + /// @notice Emitted upon a successful withdrawal of funds. + /// @param amount The total amount of ETH withdrawn (including protocol fee). event Withdraw(uint256 amount); /*============================================================== = ERRORS = ==============================================================*/ - /// @notice Thrown when the public mint stage is not active. + /// @notice Thrown when attempting to mint during a public stage that is not currently active. error PublicStageNotActive(); - /// @notice Thrown when the allowlist mint stage is not active. + /// @notice Thrown when attempting to mint during an allowlist stage that is not currently active. error AllowlistStageNotActive(); - /// @notice Thrown when the user does not send enough value. + /// @notice Thrown when the provided ETH value for a mint is insufficient. error NotEnoughValue(); - /// @notice Thrown when the wallet limit is exceeded. + /// @notice Thrown when a mint would exceed the wallet-specific minting limit. error WalletLimitExceeded(); - /// @notice Thrown when the withdraw fails. + /// @notice Thrown when a withdrawal call fails to transfer funds. error WithdrawFailed(); - /// @notice Thrown when the proof is invalid. + /// @notice Thrown when the provided Merkle proof for an allowlist mint is invalid. error InvalidProof(); - /// @notice Thrown when the stage time is invalid. + /// @notice Thrown when a stage's start or end time configuration is invalid. error InvalidStageTime(); - /// @notice Thrown when the allowlist stage is invalid. + /// @notice Thrown when the allowlist stage timing conflicts with the public stage timing. error InvalidAllowlistStageTime(); + /// @notice Thrown when the public stage timing conflicts with the allowlist stage timing. + error InvalidPublicStageTime(); + /*============================================================== = INITIALIZERS = ==============================================================*/ - /// @notice Initializes the contract. - /// @param _name The name of the token. - /// @param _symbol The symbol of the token. - /// @param _owner The owner of the contract. + /// @notice Initializes the contract with a name, symbol, and owner. + /// @dev Can only be called once. It sets the owner, emits a deploy event, and prepares the token for minting stages. + /// @param _name The ERC-721 name of the collection. + /// @param _symbol The ERC-721 symbol of the collection. + /// @param _owner The address designated as the initial owner of the contract. function initialize(string memory _name, string memory _symbol, address _owner) public initializer { __ERC721ACloneable__init(_name, _symbol); - _initializeOwner(_owner); + __ERC721MagicDropMetadataCloneable__init(_owner); emit MagicDropTokenDeployed(); } @@ -95,16 +107,19 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc = PUBLIC WRITE METHODS = ==============================================================*/ - /// @notice Mints tokens to the specified address. - /// @param qty The quantity of tokens to mint. - /// @param to The address to mint the tokens to. + /// @notice Mints tokens during the public stage. + /// @dev Requires that the current time is within the configured public stage interval. + /// Reverts if the buyer does not send enough ETH, or if the wallet limit would be exceeded. + /// @param qty The number of tokens to mint. + /// @param to The recipient address for the minted tokens. function mintPublic(uint256 qty, address to) external payable { PublicStage memory stage = _publicStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); } - if (msg.value < stage.price * qty) { + uint256 requiredPayment = stage.price * qty; + if (msg.value < requiredPayment) { revert NotEnoughValue(); } @@ -115,10 +130,12 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc _safeMint(to, qty); } - /// @notice Mints tokens to the specified address. - /// @param qty The quantity of tokens to mint. - /// @param to The address to mint the tokens to. - /// @param proof The Merkle proof for the allowlist mint stage. + /// @notice Mints tokens during the allowlist stage. + /// @dev Requires a valid Merkle proof and the current time within the allowlist stage interval. + /// Reverts if the buyer sends insufficient ETH or if the wallet limit is exceeded. + /// @param qty The number of tokens to mint. + /// @param to The recipient address for the minted tokens. + /// @param proof The Merkle proof verifying `to` is eligible for the allowlist. function mintAllowlist(uint256 qty, address to, bytes32[] calldata proof) external payable { AllowlistStage memory stage = _allowlistStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { @@ -129,7 +146,8 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert InvalidProof(); } - if (msg.value < stage.price * qty) { + uint256 requiredPayment = stage.price * qty; + if (msg.value < requiredPayment) { revert NotEnoughValue(); } @@ -140,8 +158,9 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc _safeMint(to, qty); } - /// @notice Burns a token. - /// @param tokenId The token ID to burn. + /// @notice Burns a specific token. + /// @dev Only callable by the token owner or an approved operator. The token must exist. + /// @param tokenId The ID of the token to burn. function burn(uint256 tokenId) external { _burn(tokenId, true); } @@ -150,26 +169,26 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc = PUBLIC VIEW METHODS = ==============================================================*/ - /// @notice Gets the public mint stage. - /// @return The public mint stage. + /// @notice Returns the current public stage configuration (startTime, endTime, price). + /// @return The current public stage settings. function getPublicStage() external view returns (PublicStage memory) { return _publicStage; } - /// @notice Gets the allowlist mint stage. - /// @return The allowlist mint stage. + /// @notice Returns the current allowlist stage configuration (startTime, endTime, price, merkleRoot). + /// @return The current allowlist stage settings. function getAllowlistStage() external view returns (AllowlistStage memory) { return _allowlistStage; } - /// @notice Gets the payout recipient. - /// @return The payout recipient. + /// @notice Returns the current payout recipient who receives primary sales proceeds after protocol fees. + /// @return The address currently set to receive payout funds. function payoutRecipient() external view returns (address) { return _payoutRecipient; } - /// @notice Supports the ERC721MagicDropMetadata interface. - /// @param interfaceId The interface ID. + /// @notice Indicates whether the contract implements a given interface. + /// @param interfaceId The interface ID to check for support. /// @return True if the interface is supported, false otherwise. function supportsInterface(bytes4 interfaceId) public @@ -185,72 +204,111 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc = ADMIN OPERATIONS = ==============================================================*/ - /// @notice Configures the contract with the provided setup parameters. - /// @param config The configuration parameters for setting up the contract. + /// @notice Sets up the contract parameters in a single call. + /// @dev Only callable by the owner. Configures max supply, wallet limit, URIs, stages, payout recipient, and provenance. + /// @param config A struct containing all setup parameters. function setup(SetupConfig calldata config) external onlyOwner { if (config.maxSupply > 0) { - this.setMaxSupply(config.maxSupply); + _setMaxSupply(config.maxSupply); } - // A wallet limit of 0 means unlimited mints per wallet - // Otherwise, wallets can only mint up to the specified limit if (config.walletLimit > 0) { - this.setWalletLimit(config.walletLimit); + _setWalletLimit(config.walletLimit); } if (bytes(config.baseURI).length > 0) { - this.setBaseURI(config.baseURI); + _setBaseURI(config.baseURI); } if (bytes(config.contractURI).length > 0) { - this.setContractURI(config.contractURI); + _setContractURI(config.contractURI); } - if (config.publicStage.startTime != 0 || config.publicStage.endTime != 0) { - this.setPublicStage(config.publicStage); + if (config.allowlistStage.startTime != 0 || config.allowlistStage.endTime != 0) { + _setAllowlistStage(config.allowlistStage); } - if (config.allowlistStage.startTime != 0 || config.allowlistStage.endTime != 0) { - this.setAllowlistStage(config.allowlistStage); + if (config.publicStage.startTime != 0 || config.publicStage.endTime != 0) { + _setPublicStage(config.publicStage); } if (config.payoutRecipient != address(0)) { - this.setPayoutRecipient(config.payoutRecipient); + _setPayoutRecipient(config.payoutRecipient); } if (config.provenanceHash != bytes32(0)) { - this.setProvenanceHash(config.provenanceHash); + _setProvenanceHash(config.provenanceHash); } } - /// @notice Sets the public mint stage. - /// @dev The public stage must end before the allowlist stage begins. - /// @param stage The configuration for the public mint stage. + /// @notice Sets the configuration of the public mint stage. + /// @dev Only callable by the owner. Ensures the public stage does not overlap improperly with the allowlist stage. + /// @param stage A struct defining the public stage timing and price. function setPublicStage(PublicStage calldata stage) external onlyOwner { + _setPublicStage(stage); + } + + /// @notice Sets the configuration of the allowlist mint stage. + /// @dev Only callable by the owner. Ensures the allowlist stage does not overlap improperly with the public stage. + /// @param stage A struct defining the allowlist stage timing, price, and merkle root. + function setAllowlistStage(AllowlistStage calldata stage) external onlyOwner { + _setAllowlistStage(stage); + } + + /// @notice Sets the payout recipient address for primary sale proceeds (after the protocol fee is deducted). + /// @dev Only callable by the owner. + /// @param newPayoutRecipient The address to receive future withdrawals. + function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { + _payoutRecipient = newPayoutRecipient; + } + + /// @notice Withdraws the entire contract balance, distributing protocol fees and sending the remainder to the payout recipient. + /// @dev Only callable by the owner. Reverts if transfer fails. + function withdraw() external onlyOwner { + uint256 balance = address(this).balance; + uint256 protocolFee = (balance * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; + uint256 remainingBalance = balance - protocolFee; + + (bool feeSuccess,) = PROTOCOL_FEE_RECIPIENT.call{value: protocolFee}(""); + if (!feeSuccess) revert WithdrawFailed(); + + (bool success,) = _payoutRecipient.call{value: remainingBalance}(""); + if (!success) revert WithdrawFailed(); + + emit Withdraw(balance); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Internal function to set the public mint stage configuration. + /// @dev Reverts if timing is invalid or conflicts with the allowlist stage. + /// @param stage A struct defining public stage timings and price. + function _setPublicStage(PublicStage calldata stage) internal { if (stage.startTime >= stage.endTime) { revert InvalidStageTime(); } - // If the allowlist stage is set, ensure the public stage ends before the allowlist stage begins + // Ensure no timing overlap if allowlist stage is set if (_allowlistStage.startTime != 0 && _allowlistStage.endTime != 0) { if (stage.endTime > _allowlistStage.startTime) { - revert InvalidAllowlistStageTime(); + revert InvalidPublicStageTime(); } } _publicStage = stage; } - /// @notice Sets the allowlist mint stage. - /// @dev The allowlist stage must end before the public stage begins. - /// @param stage The configuration for the allowlist mint stage. - function setAllowlistStage(AllowlistStage calldata stage) external onlyOwner { - // Validate that the start time is before the end time + /// @notice Internal function to set the allowlist mint stage configuration. + /// @dev Reverts if timing is invalid or conflicts with the public stage. + /// @param stage A struct defining allowlist stage timings, price, and merkle root. + function _setAllowlistStage(AllowlistStage calldata stage) internal { if (stage.startTime >= stage.endTime) { revert InvalidStageTime(); } - // If the public stage is set, ensure the allowlist stage ends before the public stage begins + // Ensure no timing overlap if public stage is set if (_publicStage.startTime != 0 && _publicStage.endTime != 0) { if (stage.endTime > _publicStage.startTime) { revert InvalidAllowlistStageTime(); @@ -260,43 +318,29 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc _allowlistStage = stage; } - /// @notice Sets the payout recipient. + /// @notice Internal function to set the payout recipient. + /// @dev This function does not revert if given a zero address, but no payouts would succeed if so. /// @param newPayoutRecipient The address to receive the payout from mint proceeds. - function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { + function _setPayoutRecipient(address newPayoutRecipient) internal { _payoutRecipient = newPayoutRecipient; } - /// @notice Withdraws the total mint fee and remaining balance from the contract - /// @dev Can only be called by the owner - function withdraw() external onlyOwner { - uint256 balance = address(this).balance; - uint256 protocolFee = (balance * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; - uint256 remainingBalance = balance - protocolFee; - - // Transfer protocol fee - (bool feeSuccess,) = PROTOCOL_FEE_RECIPIENT.call{value: protocolFee}(""); - if (!feeSuccess) revert WithdrawFailed(); - - // Transfer remaining balance to the payout recipient - (bool success,) = _payoutRecipient.call{value: remainingBalance}(""); - if (!success) revert WithdrawFailed(); - - emit Withdraw(balance); - } - /*============================================================== = META = ==============================================================*/ - /// @notice Returns the contract name and version - /// @return The contract name and version as strings + /// @notice Returns the contract name and version. + /// @dev Useful for external tools or metadata standards. + /// @return The contract name and version strings. function contractNameAndVersion() public pure returns (string memory, string memory) { return ("ERC721MagicDropCloneable", "1.0.0"); } - /// @notice Gets the token URI for a given token ID. - /// @param tokenId The token ID. - /// @return The token URI. + /// @notice Retrieves the token metadata URI for a given token ID. + /// @dev If no base URI is set, returns an empty string. + /// If a trailing slash is present, tokenId is appended; otherwise returns just the base URI. + /// @param tokenId The ID of the token to retrieve the URI for. + /// @return The token's metadata URI as a string. function tokenURI(uint256 tokenId) public view @@ -308,12 +352,11 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc string memory baseURI = _baseURI(); bool isBaseURIEmpty = bytes(baseURI).length == 0; - bool hasNoTrailingSlash = bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]; + bool hasNoTrailingSlash = !isBaseURIEmpty && bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]; if (isBaseURIEmpty) { return ""; } - if (hasNoTrailingSlash) { return baseURI; } @@ -325,13 +368,15 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc = MISC = ==============================================================*/ - /// @dev Overriden to prevent double-initialization of the owner. + /// @dev Overridden to allow this contract to properly manage owner initialization. + /// By always returning true, we ensure that the inherited initializer does not re-run. function _guardInitializeOwner() internal pure virtual override returns (bool) { return true; } - /// @notice Gets the starting token ID. - /// @return The starting token ID. + /// @notice Returns the token ID where enumeration starts. + /// @dev Overridden to start from token ID 1 instead of 0. + /// @return The first valid token ID. function _startTokenId() internal view virtual override returns (uint256) { return 1; } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 0d262f7..257791d 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -11,86 +11,101 @@ import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; +/// @title ERC721MagicDropMetadataCloneable +/// @notice A cloneable ERC-721A implementation that supports adjustable metadata URIs, royalty configuration, +/// and optional provenance hashing for metadata integrity. Inherits conduit-based preapprovals, +/// making distribution more gas-efficient. contract ERC721MagicDropMetadataCloneable is ERC721AConduitPreapprovedCloneable, IERC721MagicDropMetadata, ERC2981, Ownable { + function __ERC721MagicDropMetadataCloneable__init(address owner) internal onlyInitializing { + _initializeOwner(owner); + } + /*============================================================== = STORAGE = ==============================================================*/ - /// @notice The base URI for the token metadata + /// @notice The base URI used to construct `tokenURI` results. + /// @dev This value can be updated by the contract owner. Typically points to an off-chain IPFS/HTTPS endpoint. string private _tokenBaseURI; - /// @notice The contract URI for contract metadata + /// @notice A URI providing contract-level metadata (e.g., for marketplaces). + /// @dev Can be updated by the owner. Often returns metadata in a JSON format describing the project. string private _contractURI; - /// @notice The max supply of tokens to be minted + /// @notice The maximum total number of tokens that can ever be minted. + /// @dev Acts as a cap on supply. Decreasing is allowed (if no tokens are over that limit), + /// but increasing supply is forbidden after initialization. uint256 private _maxSupply; - /// @notice The max number of tokens a wallet can mint + /// @notice The per-wallet minting limit, restricting how many tokens a single address can mint. uint256 private _walletLimit; - /// @notice The provenance hash for guarenteeing metadata integrity + /// @notice A provenance hash ensuring metadata integrity and fair distribution. + /// @dev Once tokens are minted, this value cannot be changed. Commonly used to verify that + /// the metadata ordering has not been manipulated post-reveal. bytes32 private _provenanceHash; - /// @notice The royalty receiver for the collection + /// @notice The address receiving royalty payments. address private _royaltyReceiver; - /// @notice The royalty basis points for the collection + /// @notice The royalty amount (in basis points) for secondary sales (e.g., 100 = 1%). uint96 private _royaltyBps; /*============================================================== = PUBLIC VIEW METHODS = ==============================================================*/ - /// @notice Returns the base URI for the token metadata, overriding the ERC721A - /// @return The base URI for the token metadata + /// @notice Returns the current base URI used to construct token URIs. + /// @return The base URI as a string. function baseURI() public view override returns (string memory) { return _baseURI(); } - /// @notice Returns the contract URI for contract metadata - /// @return The contract URI for contract metadata + /// @notice Returns a URI representing contract-level metadata, often used by marketplaces. + /// @return The contract-level metadata URI. function contractURI() public view override returns (string memory) { return _contractURI; } - /// @notice Returns the max supply of tokens to be minted - /// @return The max supply of tokens to be minted + /// @notice The maximum number of tokens that can ever be minted by this contract. + /// @return The maximum supply of tokens. function maxSupply() public view returns (uint256) { return _maxSupply; } - /// @notice Returns the max number of tokens a wallet can mint - /// @return The max number of tokens a wallet can mint + /// @notice The maximum number of tokens any single wallet can mint. + /// @return The minting limit per wallet. function walletLimit() public view returns (uint256) { return _walletLimit; } - /// @notice Returns the provenance hash for guarenteeing metadata integrity - /// @return The provenance hash for guarenteeing metadata integrity + /// @notice The assigned provenance hash used to ensure the integrity of the metadata ordering. + /// @return The provenance hash. function provenanceHash() public view returns (bytes32) { return _provenanceHash; } - /// @notice Returns the royalty address for the collection - /// @return The royalty address for the collection + /// @notice The address designated to receive royalty payments on secondary sales. + /// @return The royalty receiver address. function royaltyAddress() public view returns (address) { return _royaltyReceiver; } - /// @notice Returns the royalty basis points for the collection - /// @return The royalty basis points for the collection + /// @notice The royalty rate in basis points (e.g. 100 = 1%) for secondary sales. + /// @return The royalty fee in basis points. function royaltyBps() public view returns (uint256) { return _royaltyBps; } - /// @notice Returns true if the contract implements the interface - /// @param interfaceId The interface ID to check - /// @return True if the contract implements the interface + /// @notice Indicates whether this contract implements a given interface. + /// @dev Supports ERC-2981 (royalties) and ERC-4906 (batch metadata updates), in addition to inherited interfaces. + /// @param interfaceId The interface ID to check for compliance. + /// @return True if the contract implements the specified interface, otherwise false. function supportsInterface(bytes4 interfaceId) public view @@ -98,8 +113,8 @@ contract ERC721MagicDropMetadataCloneable is override(ERC721ACloneable, IERC721A, ERC2981) returns (bool) { - return interfaceId == 0x2a55205a // ERC-2981 - || interfaceId == 0x49064906 // ERC-4906 + return interfaceId == 0x2a55205a // ERC-2981 royalties + || interfaceId == 0x49064906 // ERC-4906 metadata updates || super.supportsInterface(interfaceId); } @@ -107,91 +122,127 @@ contract ERC721MagicDropMetadataCloneable is = ADMIN OPERATIONS = ==============================================================*/ - /// @notice Sets the base URI for the token URIs - /// @param newBaseURI The base URI to set + /// @notice Sets a new base URI for token metadata, affecting all tokens. + /// @dev Emits a batch metadata update event if there are already minted tokens. + /// @param newBaseURI The new base URI. function setBaseURI(string calldata newBaseURI) external override onlyOwner { + _setBaseURI(newBaseURI); + } + + /// @notice Updates the contract-level metadata URI. + /// @dev Useful for marketplaces to display project details. + /// @param newContractURI The new contract metadata URI. + function setContractURI(string calldata newContractURI) external override onlyOwner { + _setContractURI(newContractURI); + } + + /// @notice Adjusts the maximum token supply. + /// @dev Cannot increase beyond the original max supply. Cannot set below the current minted amount. + /// @param newMaxSupply The new maximum supply. + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + _setMaxSupply(newMaxSupply); + } + + /// @notice Updates the per-wallet minting limit. + /// @dev This can be changed at any time to adjust distribution constraints. + /// @param newWalletLimit The new per-wallet limit on minted tokens. + function setWalletLimit(uint256 newWalletLimit) external onlyOwner { + _setWalletLimit(newWalletLimit); + } + + /// @notice Sets the provenance hash, used to verify metadata integrity and prevent tampering. + /// @dev Can only be set before any tokens are minted. + /// @param newProvenanceHash The new provenance hash. + function setProvenanceHash(bytes32 newProvenanceHash) external onlyOwner { + _setProvenanceHash(newProvenanceHash); + } + + /// @notice Configures the royalty information for secondary sales. + /// @dev Sets a new receiver and basis points for royalties. Basis points define the percentage rate. + /// @param newReceiver The address to receive royalties. + /// @param newBps The royalty rate in basis points (e.g., 100 = 1%). + function setRoyaltyInfo(address newReceiver, uint96 newBps) external onlyOwner { + _setRoyaltyInfo(newReceiver, newBps); + } + + /// @notice Emits an event to notify clients of metadata changes for a specific token range. + /// @dev Useful for updating external indexes after significant metadata alterations. + /// @param fromTokenId The starting token ID in the updated range. + /// @param toTokenId The ending token ID in the updated range. + function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external onlyOwner { + emit BatchMetadataUpdate(fromTokenId, toTokenId); + } + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + /// @notice Internal function returning the current base URI for token metadata. + /// @return The current base URI string. + function _baseURI() internal view override returns (string memory) { + return _tokenBaseURI; + } + + /// @notice Internal function setting the base URI for token metadata. + /// @param newBaseURI The new base URI string. + function _setBaseURI(string calldata newBaseURI) internal { _tokenBaseURI = newBaseURI; if (totalSupply() != 0) { + // Notify EIP-4906 compliant observers of a metadata update. emit BatchMetadataUpdate(0, totalSupply() - 1); } } - /// @notice Sets the contract URI for contract metadata - /// @param newContractURI The contract URI to set - function setContractURI(string calldata newContractURI) external override onlyOwner { - _contractURI = newContractURI; - - emit ContractURIUpdated(newContractURI); - } - - /// @notice Sets the max supply of tokens to be minted - /// @param newMaxSupply The max supply of tokens to be minted - function setMaxSupply(uint256 newMaxSupply) external onlyOwner { - // Ensure the new max supply is not greater than the current max supply - if (newMaxSupply > _maxSupply) { + /// @notice Internal function setting the maximum token supply. + /// @dev Cannot increase beyond the original max supply. Cannot set below the current minted amount. + /// @param newMaxSupply The new maximum supply. + function _setMaxSupply(uint256 newMaxSupply) internal { + if (_maxSupply != 0 && newMaxSupply > _maxSupply) { revert MaxSupplyCannotBeIncreased(); } - // Ensure the new max supply is greater than the current supply if (newMaxSupply < totalSupply()) { revert MaxSupplyCannotBeLessThanCurrentSupply(); } _maxSupply = newMaxSupply; - emit MaxSupplyUpdated(newMaxSupply); } - /// @notice Sets the max number of tokens a wallet can mint - /// @param newWalletLimit The max number of tokens a wallet can mint - function setWalletLimit(uint256 newWalletLimit) external onlyOwner { + /// @notice Internal function setting the per-wallet minting limit. + /// @param newWalletLimit The new per-wallet limit on minted tokens. + function _setWalletLimit(uint256 newWalletLimit) internal { _walletLimit = newWalletLimit; - emit WalletLimitUpdated(newWalletLimit); } - /// @notice Sets the provenance hash for guarenteeing metadata integrity - /// for random reveals. Created using a hash of the metadata. - /// Reverts if the provenance hash is updated after any tokens have been minted. - /// @param newProvenanceHash The provenance hash to set - function setProvenanceHash(bytes32 newProvenanceHash) external onlyOwner { + /// @notice Internal function setting the provenance hash. + /// @param newProvenanceHash The new provenance hash. + function _setProvenanceHash(bytes32 newProvenanceHash) internal { if (_totalMinted() > 0) { revert ProvenanceHashCannotBeUpdated(); } bytes32 oldProvenanceHash = _provenanceHash; _provenanceHash = newProvenanceHash; - emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); } - /// @notice Sets the royalty info for the contract - /// @param newReceiver The address to receive royalties - /// @param newBps The royalty basis points (100 = 1%) - function setRoyaltyInfo(address newReceiver, uint96 newBps) external onlyOwner { + /// @notice Internal function setting the royalty information. + /// @param newReceiver The address to receive royalties. + /// @param newBps The royalty rate in basis points (e.g., 100 = 1%). + function _setRoyaltyInfo(address newReceiver, uint96 newBps) internal { _royaltyReceiver = newReceiver; _royaltyBps = newBps; - super._setDefaultRoyalty(_royaltyReceiver, _royaltyBps); - emit RoyaltyInfoUpdated(_royaltyReceiver, _royaltyBps); } - /// @notice Emit an event notifying metadata updates for a range of token ids (EIP-4906) - /// @param fromTokenId The start token id. - /// @param toTokenId The end token id. - function emitBatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId) external onlyOwner { - emit BatchMetadataUpdate(fromTokenId, toTokenId); - } - - /*============================================================== - = INTERNAL HELPERS = - ==============================================================*/ - - /// @notice Returns the base URI for the token metadata, overriding the ERC721A - /// @return The base URI for the token metadata - function _baseURI() internal view override returns (string memory) { - return _tokenBaseURI; + /// @notice Internal function setting the contract URI. + /// @param newContractURI The new contract metadata URI. + function _setContractURI(string calldata newContractURI) internal { + _contractURI = newContractURI; + emit ContractURIUpdated(newContractURI); } } diff --git a/contracts/nft/erc721m/clones/Types.sol b/contracts/nft/erc721m/clones/Types.sol index 5f7a5e2..31fcf45 100644 --- a/contracts/nft/erc721m/clones/Types.sol +++ b/contracts/nft/erc721m/clones/Types.sol @@ -40,12 +40,6 @@ struct SetupConfig { AllowlistStage allowlistStage; /// @dev The payout recipient of the token. address payoutRecipient; - /// @dev The recipient of mint fees. - /// @notice Mint fees are sent when funds are withdrawn. - address feeRecipient; - /// @dev The mint fee. - /// @notice This is the fee charged for each mint. - uint256 mintFee; /// @dev The provenance hash of the token. /// @notice This is used to ensure the metadata is not tampered with. /// A value of 0 is used to indicate that the provenance hash is not set. diff --git a/foundry.toml b/foundry.toml index 1c70be4..2b323b6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,3 +12,4 @@ optimizer_runs = 777 [etherscan] apechain = {key = "${VERIFICATION_API_KEY_APECHAIN}", chain = 33139, url = "https://api.apescan.io/api"} sei = {key = "${VERIFICATION_API_KEY_SEI}", chain = 1329, url = "https://api.seiscan.io/api"} + diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol new file mode 100644 index 0000000..f52953a --- /dev/null +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; + +import {LibClone} from "solady/src/utils/LibClone.sol"; +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; + +import {ERC721MagicDropCloneable} from "contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol"; +import {PublicStage, AllowlistStage, SetupConfig} from "contracts/nft/erc721m/clones/Types.sol"; +import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; + +// Dummy merkle proof generation utilities for testing +contract MerkleTestHelper { + // This is a placeholder helper. In a real test, you'd generate a real merkle tree offline. + // Here we hardcode a single allowlisted address and its proof. + bytes32[] internal proof; + bytes32 internal root; + address internal allowedAddr; + + constructor() { + allowedAddr = address(0xABCD); + // For simplicity, root = keccak256(abi.encodePacked(allowedAddr)) + // Proof is empty since this is a single-leaf tree. + root = keccak256(abi.encodePacked(allowedAddr)); + } + + function getRoot() external view returns (bytes32) { + return root; + } + + function getProofFor(address addr) external view returns (bytes32[] memory) { + if (addr == allowedAddr) { + // Single-leaf tree: no proof necessary except empty array + return new bytes32[](0); + } else { + // No valid proof + bytes32[] memory emptyProof; + return emptyProof; + } + } + + function getAllowedAddress() external view returns (address) { + return allowedAddr; + } +} + +contract ERC721MagicDropCloneableTest is Test { + ERC721MagicDropCloneable public token; + MerkleTestHelper public merkleHelper; + + address internal owner = address(0x1234); + address internal user = address(0x1111); + address internal user2 = address(0x2222); + address internal payoutRecipient = address(0x9999); + uint256 internal initialStartTime; + uint256 internal initialEndTime; + + function setUp() public { + token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable()))); + merkleHelper = new MerkleTestHelper(); + + // Initialize token + token.initialize("TestToken", "TT", owner); + + // Default stages + initialStartTime = block.timestamp + 100; + initialEndTime = block.timestamp + 200; + + SetupConfig memory config = SetupConfig({ + maxSupply: 1000, + walletLimit: 5, + baseURI: "https://example.com/metadata/", + contractURI: "https://example.com/contract-metadata.json", + publicStage: PublicStage({ + startTime: uint64(initialStartTime), + endTime: uint64(initialEndTime), + price: 0.01 ether + }), + allowlistStage: AllowlistStage({ + startTime: uint64(initialEndTime + 100), + endTime: uint64(initialEndTime + 200), + price: 0.005 ether, + merkleRoot: merkleHelper.getRoot() + }), + payoutRecipient: payoutRecipient, + provenanceHash: keccak256("some-provenance") + }); + + vm.prank(owner); + token.setup(config); + } + + /*============================================================== + = TEST INITIALIZATION = + ==============================================================*/ + + function testInitialization() public { + assertEq(token.owner(), owner); + assertEq(token.name(), "TestToken"); + assertEq(token.symbol(), "TT"); + assertEq(token.payoutRecipient(), payoutRecipient); + } + + function testMultipleInitializationReverts() public { + vm.prank(owner); + vm.expectRevert(); // The contract should revert if trying to re-initialize + token.initialize("ReInit", "RI", owner); + } + + /*============================================================== + = TEST PUBLIC MINTING STAGE = + ==============================================================*/ + + function testMintPublicHappyPath() public { + // Move to public sale time + vm.warp(initialStartTime + 1); + + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.01 ether}(1, user); + + assertEq(token.balanceOf(user), 1); + } + + function testMintPublicBeforeStartReverts() public { + // Before start + vm.warp(initialStartTime - 10); + vm.deal(user, 1 ether); + + vm.prank(user); + vm.expectRevert(ERC721MagicDropCloneable.PublicStageNotActive.selector); + token.mintPublic{value: 0.01 ether}(1, user); + } + + function testMintPublicAfterEndReverts() public { + // After end + vm.warp(initialEndTime + 10); + vm.deal(user, 1 ether); + + vm.prank(user); + vm.expectRevert(ERC721MagicDropCloneable.PublicStageNotActive.selector); + token.mintPublic{value: 0.01 ether}(1, user); + } + + function testMintPublicNotEnoughValueReverts() public { + vm.warp(initialStartTime + 1); + vm.deal(user, 0.005 ether); + + vm.prank(user); + vm.expectRevert(ERC721MagicDropCloneable.NotEnoughValue.selector); + token.mintPublic{value: 0.005 ether}(1, user); + } + + function testMintPublicWalletLimitExceededReverts() public { + vm.warp(initialStartTime + 1); + vm.deal(user, 1 ether); + + vm.startPrank(user); + // Mint up to the limit (5) + token.mintPublic{value: 0.05 ether}(5, user); + assertEq(token.balanceOf(user), 5); + + // Attempt to mint one more + vm.expectRevert(ERC721MagicDropCloneable.WalletLimitExceeded.selector); + token.mintPublic{value: 0.01 ether}(1, user); + vm.stopPrank(); + } + + /*============================================================== + = TEST ALLOWLIST MINTING STAGE = + ==============================================================*/ + + function testMintAllowlistHappyPath() public { + // Move time to allowlist + uint256 allowlistStart = initialEndTime + 100; + vm.warp(allowlistStart + 1); + + vm.deal(merkleHelper.getAllowedAddress(), 1 ether); + vm.prank(merkleHelper.getAllowedAddress()); + token.mintAllowlist{value: 0.005 ether}( + 1, merkleHelper.getAllowedAddress(), merkleHelper.getProofFor(merkleHelper.getAllowedAddress()) + ); + + assertEq(token.balanceOf(merkleHelper.getAllowedAddress()), 1); + } + + function testMintAllowlistInvalidProofReverts() public { + uint256 allowlistStart = initialEndTime + 100; + vm.warp(allowlistStart + 1); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + + vm.deal(allowedAddr, 1 ether); + vm.prank(allowedAddr); + + vm.expectRevert(ERC721MagicDropCloneable.InvalidProof.selector); + token.mintAllowlist{value: 0.005 ether}(1, user, proof); + } + + function testMintAllowlistNotActiveReverts() public { + // Before allowlist start + uint256 allowlistStart = initialEndTime + 100; + vm.warp(allowlistStart - 10); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + vm.deal(allowedAddr, 1 ether); + vm.prank(allowedAddr); + + vm.expectRevert(ERC721MagicDropCloneable.AllowlistStageNotActive.selector); + token.mintAllowlist{value: 0.005 ether}(1, allowedAddr, proof); + } + + function testMintAllowlistNotEnoughValueReverts() public { + uint256 allowlistStart = initialEndTime + 100; + vm.warp(allowlistStart + 1); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + vm.deal(allowedAddr, 0.001 ether); + vm.prank(allowedAddr); + + vm.expectRevert(ERC721MagicDropCloneable.NotEnoughValue.selector); + token.mintAllowlist{value: 0.001 ether}(1, allowedAddr, proof); + } + + function testMintAllowlistWalletLimitExceededReverts() public { + uint256 allowlistStart = initialEndTime + 100; + vm.warp(allowlistStart + 1); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + vm.deal(allowedAddr, 1 ether); + + vm.startPrank(allowedAddr); + // Mint up to the limit + token.mintAllowlist{value: 0.025 ether}(5, allowedAddr, proof); + assertEq(token.balanceOf(allowedAddr), 5); + + vm.expectRevert(ERC721MagicDropCloneable.WalletLimitExceeded.selector); + token.mintAllowlist{value: 0.005 ether}(1, allowedAddr, proof); + vm.stopPrank(); + } + + /*============================================================== + = BURNING = + ==============================================================*/ + + function testBurnHappyPath() public { + // Public mint first + vm.warp(initialStartTime + 1); + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.01 ether}(1, user); + + uint256 tokenId = 1; + assertEq(token.ownerOf(tokenId), user); + + vm.prank(user); + token.burn(tokenId); + + vm.expectRevert(); + token.ownerOf(tokenId); + } + + function testBurnInvalidTokenReverts() public { + vm.prank(user); + vm.expectRevert(); + token.burn(9999); // non-existent token + } + + function testBurnNotOwnerReverts() public { + // mint to user + vm.warp(initialStartTime + 1); + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.01 ether}(1, user); + uint256 tokenId = 1; + + vm.prank(user2); + vm.expectRevert(); + token.burn(tokenId); + } + + /*============================================================== + = GETTERS = + ==============================================================*/ + + function testGetPublicStage() public { + PublicStage memory ps = token.getPublicStage(); + assertEq(ps.startTime, initialStartTime); + assertEq(ps.endTime, initialEndTime); + assertEq(ps.price, 0.01 ether); + } + + function testGetAllowlistStage() public { + AllowlistStage memory als = token.getAllowlistStage(); + assertEq(als.startTime, initialEndTime + 100); + assertEq(als.endTime, initialEndTime + 200); + assertEq(als.price, 0.005 ether); + assertEq(als.merkleRoot, merkleHelper.getRoot()); + } + + function testPayoutRecipient() public { + assertEq(token.payoutRecipient(), payoutRecipient); + } + + /*============================================================== + = SUPPORTSINTERFACE = + ==============================================================*/ + + function testSupportsInterface() public { + // Just checks a known supported interface + assertTrue(token.supportsInterface(type(IERC721MagicDropMetadata).interfaceId)); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + function testSetPublicStageInvalidTimesReverts() public { + PublicStage memory invalidStage = PublicStage({ + startTime: uint64(block.timestamp + 1000), + endTime: uint64(block.timestamp + 500), // end before start + price: 0.01 ether + }); + + vm.prank(owner); + vm.expectRevert(ERC721MagicDropCloneable.InvalidStageTime.selector); + token.setPublicStage(invalidStage); + } + + function testSetAllowlistStageInvalidTimesReverts() public { + AllowlistStage memory invalidStage = AllowlistStage({ + startTime: uint64(block.timestamp + 1000), + endTime: uint64(block.timestamp + 500), // end before start + price: 0.005 ether, + merkleRoot: merkleHelper.getRoot() + }); + + vm.prank(owner); + vm.expectRevert(ERC721MagicDropCloneable.InvalidStageTime.selector); + token.setAllowlistStage(invalidStage); + } + + function testSetPublicStageOverlapWithAllowlistReverts() public { + // Current allowlist starts at initialEndTime+100 + // Try to set public stage that ends after that + PublicStage memory overlappingStage = PublicStage({ + startTime: uint64(block.timestamp + 10), + endTime: uint64(initialEndTime + 150), + price: 0.01 ether + }); + + vm.prank(owner); + vm.expectRevert(ERC721MagicDropCloneable.InvalidPublicStageTime.selector); + token.setPublicStage(overlappingStage); + } + + function testSetAllowlistStageOverlapWithPublicReverts() public { + // Current public ends at initialEndTime + // Try to set allowlist that ends before public ends + AllowlistStage memory overlappingStage = AllowlistStage({ + startTime: uint64(initialEndTime - 50), + endTime: uint64(initialEndTime + 10), + price: 0.005 ether, + merkleRoot: merkleHelper.getRoot() + }); + + vm.prank(owner); + vm.expectRevert(ERC721MagicDropCloneable.InvalidAllowlistStageTime.selector); + token.setAllowlistStage(overlappingStage); + } + + function testSetPayoutRecipient() public { + vm.prank(owner); + token.setPayoutRecipient(address(0x8888)); + assertEq(token.payoutRecipient(), address(0x8888)); + } + + function testWithdraw() public { + // Mint some tokens to generate balance + vm.warp(initialStartTime + 1); + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.05 ether}(5, user); // 5 * 0.01 = 0.05 ether + + uint256 balanceBefore = payoutRecipient.balance; + uint256 contractBal = address(token).balance; + assertEq(contractBal, 0.05 ether); + + vm.prank(owner); + token.withdraw(); + + uint256 balanceAfter = payoutRecipient.balance; + // Protocol fee = 5% = 0.00025 ether + // Remaining to payout = 0.0475 ether + assertEq(balanceAfter - balanceBefore, 0.0475 ether); + assertEq(address(token).balance, 0); + } + + /*============================================================== + = METADATA = + ==============================================================*/ + + function testContractNameAndVersion() public { + (string memory n, string memory v) = token.contractNameAndVersion(); + assertEq(n, "ERC721MagicDropCloneable"); + assertEq(v, "1.0.0"); + } + + function testTokenURI() public { + // Mint token #1 + vm.warp(initialStartTime + 1); + vm.deal(user, 1 ether); + vm.prank(user); + token.mintPublic{value: 0.01 ether}(1, user); + string memory uri = token.tokenURI(1); + assertEq(uri, "https://example.com/metadata/1"); + } + + function testTokenURIForNonexistentTokenReverts() public { + vm.expectRevert(); + token.tokenURI(9999); + } +} From 2ed03061ad267d3473a67fd2d747bfd1339442dd Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 9 Dec 2024 21:52:03 -0500 Subject: [PATCH 13/26] cleanup Signed-off-by: Adam Wolf --- cli/cmds/utils | 10 - .../ERC721MagicDropMetadataCloneable.sol | 7 + .../ERC721MagicDropMetadataCloneable.t.sol | 280 ++++++++++++++++++ 3 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol diff --git a/cli/cmds/utils b/cli/cmds/utils index 5d40d93..2ec2ced 100644 --- a/cli/cmds/utils +++ b/cli/cmds/utils @@ -101,16 +101,6 @@ save_deployment_data() { } prestart() { - # Check Node.js version - NODE_VERSION=$(node -v) - REQUIRED_VERSION="v20.9.0" - if [[ "$(printf '%s\n' "$REQUIRED_VERSION" "$NODE_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]]; then - echo "Node.js version is $NODE_VERSION. Please upgrade to version 20.9 or greater." - echo "You can use nvm to install and switch versions: https://github.com/nvm-sh/nvm" - echo "nvm install 20.9 or nvm use 20.9" - exit 1 - fi - # Fetch the latest changes from the remote git -C "$BASE_DIR" fetch diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 257791d..aca7b46 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -21,6 +21,13 @@ contract ERC721MagicDropMetadataCloneable is ERC2981, Ownable { + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + /// @notice Initializes the contract. + /// @dev This function is called by the initializer of the parent contract. + /// @param owner The address of the contract owner. function __ERC721MagicDropMetadataCloneable__init(address owner) internal onlyInitializing { _initializeOwner(owner); } diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol new file mode 100644 index 0000000..6cbee54 --- /dev/null +++ b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; + +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {LibClone} from "solady/src/utils/LibClone.sol"; + +import {ERC721MagicDropMetadataCloneable} from "contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol"; +import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; + +interface IERC2981 { + function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address, uint256); +} + +/// @dev A testable contract that exposes a mint function for testing scenarios that depend on having minted tokens. +contract TestableERC721MagicDropMetadataCloneable is ERC721MagicDropMetadataCloneable { + function initialize(address _owner) external initializer { + __ERC721MagicDropMetadataCloneable__init(_owner); + } + + function mintForTest(address to, uint256 quantity) external onlyOwner { + _mint(to, quantity); + } +} + +contract ERC721MagicDropMetadataCloneableTest is Test { + TestableERC721MagicDropMetadataCloneable token; + + address owner = address(0x1234); + address user = address(0xABCD); + address royaltyReceiver = address(0x9999); + + function setUp() public { + token = TestableERC721MagicDropMetadataCloneable( + LibClone.deployERC1967(address(new TestableERC721MagicDropMetadataCloneable())) + ); + token.initialize(owner); + } + + /*============================================================== + = INITIALIZATION = + ==============================================================*/ + + function testInitialization() public { + assertEq(token.owner(), owner); + assertEq(token.maxSupply(), 0); + assertEq(token.walletLimit(), 0); + assertEq(token.provenanceHash(), bytes32(0)); + assertEq(token.baseURI(), ""); + assertEq(token.contractURI(), ""); + assertEq(token.royaltyAddress(), address(0)); + assertEq(token.royaltyBps(), 0); + } + + /*============================================================== + = ONLY OWNER TESTS = + ==============================================================*/ + + function testOnlyOwnerFunctions() public { + // Try calling setBaseURI as user + vm.prank(user); + vm.expectRevert(Ownable.Unauthorized.selector); + token.setBaseURI("ipfs://newbase/"); + + // Similarly test contractURI + vm.prank(user); + vm.expectRevert(Ownable.Unauthorized.selector); + token.setContractURI("https://new-contract-uri.json"); + } + + /*============================================================== + = BASE URI = + ==============================================================*/ + + function testSetBaseURIWhenNoTokensMinted() public { + vm.prank(owner); + token.setBaseURI("https://example.com/metadata/"); + assertEq(token.baseURI(), "https://example.com/metadata/"); + // No tokens minted, so no BatchMetadataUpdate event expected + } + + function testSetBaseURIWithTokensMinted() public { + // Mint some tokens first + vm.startPrank(owner); + token.mintForTest(user, 5); // now totalSupply = 5 + vm.expectEmit(true, true, true, true); + emit IERC721MagicDropMetadata.BatchMetadataUpdate(0, 4); + token.setBaseURI("https://example.com/metadata/"); + vm.stopPrank(); + + assertEq(token.baseURI(), "https://example.com/metadata/"); + } + + /*============================================================== + = CONTRACT URI = + ==============================================================*/ + + function testSetContractURI() public { + vm.prank(owner); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.ContractURIUpdated("https://new-contract-uri.json"); + token.setContractURI("https://new-contract-uri.json"); + assertEq(token.contractURI(), "https://new-contract-uri.json"); + } + + function testSetEmptyContractURI() public { + vm.prank(owner); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.ContractURIUpdated(""); + token.setContractURI(""); + assertEq(token.contractURI(), ""); + } + + /*============================================================== + = MAX SUPPLY = + ==============================================================*/ + + function testSetMaxSupplyBasic() public { + vm.prank(owner); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.MaxSupplyUpdated(1000); + token.setMaxSupply(1000); + assertEq(token.maxSupply(), 1000); + } + + function testSetMaxSupplyDecreaseNotBelowMinted() public { + vm.startPrank(owner); + token.mintForTest(user, 10); + // Currently minted = 10 + vm.expectRevert(IERC721MagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); + token.setMaxSupply(5); + + // Setting exactly to 10 should pass + token.setMaxSupply(10); + assertEq(token.maxSupply(), 10); + } + + function testSetMaxSupplyCannotIncreaseBeyondOriginal() public { + vm.startPrank(owner); + token.setMaxSupply(1000); + vm.expectRevert(IERC721MagicDropMetadata.MaxSupplyCannotBeIncreased.selector); + token.setMaxSupply(2000); + } + + /*============================================================== + = WALLET LIMIT = + ==============================================================*/ + + function testSetWalletLimit() public { + vm.prank(owner); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.WalletLimitUpdated(20); + token.setWalletLimit(20); + assertEq(token.walletLimit(), 20); + } + + function testSetZeroWalletLimit() public { + vm.prank(owner); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.WalletLimitUpdated(0); + token.setWalletLimit(0); + assertEq(token.walletLimit(), 0); + } + + /*============================================================== + = PROVENANCE HASH = + ==============================================================*/ + + function testSetProvenanceHashBeforeMint() public { + vm.prank(owner); + bytes32 ph = keccak256("test-provenance"); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.ProvenanceHashUpdated(bytes32(0), ph); + token.setProvenanceHash(ph); + assertEq(token.provenanceHash(), ph); + } + + function testSetProvenanceHashAfterMintReverts() public { + vm.startPrank(owner); + token.mintForTest(user, 1); // now minted = 1 + bytes32 ph = keccak256("another-ph"); + vm.expectRevert(IERC721MagicDropMetadata.ProvenanceHashCannotBeUpdated.selector); + token.setProvenanceHash(ph); + } + + /*============================================================== + = ROYALTY INFO = + ==============================================================*/ + + function testSetRoyaltyInfo() public { + vm.prank(owner); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.RoyaltyInfoUpdated(royaltyReceiver, 500); + token.setRoyaltyInfo(royaltyReceiver, 500); + + assertEq(token.royaltyAddress(), royaltyReceiver); + assertEq(token.royaltyBps(), 500); + + // Check ERC2981 royaltyInfo + (address receiver, uint256 amount) = IERC2981(address(token)).royaltyInfo(1, 10_000); + assertEq(receiver, royaltyReceiver); + assertEq(amount, 500); // 5% of 10000 = 500 + } + + function testSetRoyaltyInfoZeroAddress() public { + vm.prank(owner); + + vm.expectRevert(); + token.setRoyaltyInfo(address(0), 1000); + } + + /*============================================================== + = BATCH METADATA UPDATES = + ==============================================================*/ + + function testEmitBatchMetadataUpdate() public { + // Mint some tokens + vm.startPrank(owner); + token.mintForTest(user, 10); + + vm.expectEmit(true, true, true, true); + emit IERC721MagicDropMetadata.BatchMetadataUpdate(2, 5); + token.emitBatchMetadataUpdate(2, 5); + vm.stopPrank(); + } + + /*============================================================== + = SUPPORTS INTERFACE = + ==============================================================*/ + + function testSupportsInterface() public { + // ERC2981 interfaceId = 0x2a55205a + assertTrue(token.supportsInterface(0x2a55205a)); + // ERC4906 interfaceId = 0x49064906 + assertTrue(token.supportsInterface(0x49064906)); + // Some random interface + assertFalse(token.supportsInterface(0x12345678)); + } + + /*============================================================== + = EDGE CASE TESTS = + ==============================================================*/ + + // If we never set maxSupply initially, setting it to something smaller than minted is invalid + function testCannotSetMaxSupplyLessThanMintedEvenIfNotSetBefore() public { + vm.startPrank(owner); + token.mintForTest(user, 5); + vm.expectRevert(IERC721MagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); + token.setMaxSupply(1); + } + + function testSettingProvenanceHashTwiceBeforeMintIsAllowed() public { + vm.startPrank(owner); + bytes32 ph1 = keccak256("ph1"); + token.setProvenanceHash(ph1); + assertEq(token.provenanceHash(), ph1); + + // Setting again before mint (though unusual) is possible since no tokens minted yet + bytes32 ph2 = keccak256("ph2"); + vm.expectEmit(false, false, false, true); + emit IERC721MagicDropMetadata.ProvenanceHashUpdated(ph1, ph2); + token.setProvenanceHash(ph2); + assertEq(token.provenanceHash(), ph2); + } + + function testSetBaseURIEmptyString() public { + vm.prank(owner); + token.setBaseURI(""); + assertEq(token.baseURI(), ""); + } + + function testSetMaxSupplyToCurrentSupply() public { + vm.startPrank(owner); + token.mintForTest(user, 10); + token.setMaxSupply(10); + assertEq(token.maxSupply(), 10); + } +} From 1b7861c2652b3593232bba0d3fd87af95a81f62c Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 10 Dec 2024 16:04:19 -0500 Subject: [PATCH 14/26] enforce private before public, and transfer proceeds on mint Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.sol | 56 +++++--- .../clones/ERC721MagicDropCloneable.t.sol | 121 +++++++++++------- 2 files changed, 112 insertions(+), 65 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index c2e31a7..aace594 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.22; import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; import {IERC721A} from "erc721a/contracts/IERC721A.sol"; @@ -87,6 +88,9 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// @notice Thrown when the public stage timing conflicts with the allowlist stage timing. error InvalidPublicStageTime(); + /// @notice Thrown when the payout recipient is set to a zero address. + error PayoutRecipientCannotBeZeroAddress(); + /*============================================================== = INITIALIZERS = ==============================================================*/ @@ -127,6 +131,10 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert WalletLimitExceeded(); } + if (stage.price != 0) { + _splitProceeds(); + } + _safeMint(to, qty); } @@ -155,6 +163,10 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert WalletLimitExceeded(); } + if (stage.price != 0) { + _splitProceeds(); + } + _safeMint(to, qty); } @@ -262,22 +274,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc _payoutRecipient = newPayoutRecipient; } - /// @notice Withdraws the entire contract balance, distributing protocol fees and sending the remainder to the payout recipient. - /// @dev Only callable by the owner. Reverts if transfer fails. - function withdraw() external onlyOwner { - uint256 balance = address(this).balance; - uint256 protocolFee = (balance * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; - uint256 remainingBalance = balance - protocolFee; - - (bool feeSuccess,) = PROTOCOL_FEE_RECIPIENT.call{value: protocolFee}(""); - if (!feeSuccess) revert WithdrawFailed(); - - (bool success,) = _payoutRecipient.call{value: remainingBalance}(""); - if (!success) revert WithdrawFailed(); - - emit Withdraw(balance); - } - /*============================================================== = INTERNAL HELPERS = ==============================================================*/ @@ -290,9 +286,9 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert InvalidStageTime(); } - // Ensure no timing overlap if allowlist stage is set + // Ensure the public stage starts after the allowlist stage ends if (_allowlistStage.startTime != 0 && _allowlistStage.endTime != 0) { - if (stage.endTime > _allowlistStage.startTime) { + if (stage.startTime < _allowlistStage.endTime) { revert InvalidPublicStageTime(); } } @@ -308,7 +304,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert InvalidStageTime(); } - // Ensure no timing overlap if public stage is set + // Ensure the public stage starts after the allowlist stage ends if (_publicStage.startTime != 0 && _publicStage.endTime != 0) { if (stage.endTime > _publicStage.startTime) { revert InvalidAllowlistStageTime(); @@ -325,6 +321,28 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc _payoutRecipient = newPayoutRecipient; } + /// @notice Internal function to split the proceeds of a mint. + /// @dev This function is called by the mint functions to split the proceeds into a protocol fee and a payout. + function _splitProceeds() internal { + if (_payoutRecipient == address(0)) { + revert PayoutRecipientCannotBeZeroAddress(); + } + + uint256 protocolFee = (msg.value * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; + + /// @dev Remaining balance is the balance minus the protocol fee. + uint256 remainingBalance; + unchecked { + remainingBalance = msg.value - protocolFee; + } + + /// @dev Transfer the protocol fee to the protocol fee recipient. + SafeTransferLib.safeTransferETH(PROTOCOL_FEE_RECIPIENT, protocolFee); + + /// @dev Transfer the remaining balance to the payout recipient. + SafeTransferLib.safeTransferETH(_payoutRecipient, remainingBalance); + } + /*============================================================== = META = ==============================================================*/ diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index f52953a..0ca0229 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -54,8 +54,10 @@ contract ERC721MagicDropCloneableTest is Test { address internal user = address(0x1111); address internal user2 = address(0x2222); address internal payoutRecipient = address(0x9999); - uint256 internal initialStartTime; - uint256 internal initialEndTime; + uint256 internal publicStart; + uint256 internal publicEnd; + uint256 internal allowlistStart; + uint256 internal allowlistEnd; function setUp() public { token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable()))); @@ -65,25 +67,28 @@ contract ERC721MagicDropCloneableTest is Test { token.initialize("TestToken", "TT", owner); // Default stages - initialStartTime = block.timestamp + 100; - initialEndTime = block.timestamp + 200; + allowlistStart = block.timestamp + 100; + allowlistEnd = block.timestamp + 200; + + publicStart = block.timestamp + 300; + publicEnd = block.timestamp + 400; SetupConfig memory config = SetupConfig({ maxSupply: 1000, walletLimit: 5, baseURI: "https://example.com/metadata/", contractURI: "https://example.com/contract-metadata.json", - publicStage: PublicStage({ - startTime: uint64(initialStartTime), - endTime: uint64(initialEndTime), - price: 0.01 ether - }), allowlistStage: AllowlistStage({ - startTime: uint64(initialEndTime + 100), - endTime: uint64(initialEndTime + 200), + startTime: uint64(allowlistStart), + endTime: uint64(allowlistEnd), price: 0.005 ether, merkleRoot: merkleHelper.getRoot() }), + publicStage: PublicStage({ + startTime: uint64(publicStart), + endTime: uint64(publicEnd), + price: 0.01 ether + }), payoutRecipient: payoutRecipient, provenanceHash: keccak256("some-provenance") }); @@ -115,7 +120,7 @@ contract ERC721MagicDropCloneableTest is Test { function testMintPublicHappyPath() public { // Move to public sale time - vm.warp(initialStartTime + 1); + vm.warp(publicStart + 1); vm.deal(user, 1 ether); @@ -127,7 +132,7 @@ contract ERC721MagicDropCloneableTest is Test { function testMintPublicBeforeStartReverts() public { // Before start - vm.warp(initialStartTime - 10); + vm.warp(publicStart - 10); vm.deal(user, 1 ether); vm.prank(user); @@ -137,7 +142,7 @@ contract ERC721MagicDropCloneableTest is Test { function testMintPublicAfterEndReverts() public { // After end - vm.warp(initialEndTime + 10); + vm.warp(publicEnd + 10); vm.deal(user, 1 ether); vm.prank(user); @@ -146,7 +151,7 @@ contract ERC721MagicDropCloneableTest is Test { } function testMintPublicNotEnoughValueReverts() public { - vm.warp(initialStartTime + 1); + vm.warp(publicStart + 1); vm.deal(user, 0.005 ether); vm.prank(user); @@ -155,7 +160,7 @@ contract ERC721MagicDropCloneableTest is Test { } function testMintPublicWalletLimitExceededReverts() public { - vm.warp(initialStartTime + 1); + vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.startPrank(user); @@ -175,7 +180,6 @@ contract ERC721MagicDropCloneableTest is Test { function testMintAllowlistHappyPath() public { // Move time to allowlist - uint256 allowlistStart = initialEndTime + 100; vm.warp(allowlistStart + 1); vm.deal(merkleHelper.getAllowedAddress(), 1 ether); @@ -188,7 +192,6 @@ contract ERC721MagicDropCloneableTest is Test { } function testMintAllowlistInvalidProofReverts() public { - uint256 allowlistStart = initialEndTime + 100; vm.warp(allowlistStart + 1); address allowedAddr = merkleHelper.getAllowedAddress(); @@ -203,7 +206,6 @@ contract ERC721MagicDropCloneableTest is Test { function testMintAllowlistNotActiveReverts() public { // Before allowlist start - uint256 allowlistStart = initialEndTime + 100; vm.warp(allowlistStart - 10); address allowedAddr = merkleHelper.getAllowedAddress(); @@ -216,7 +218,6 @@ contract ERC721MagicDropCloneableTest is Test { } function testMintAllowlistNotEnoughValueReverts() public { - uint256 allowlistStart = initialEndTime + 100; vm.warp(allowlistStart + 1); address allowedAddr = merkleHelper.getAllowedAddress(); @@ -229,7 +230,6 @@ contract ERC721MagicDropCloneableTest is Test { } function testMintAllowlistWalletLimitExceededReverts() public { - uint256 allowlistStart = initialEndTime + 100; vm.warp(allowlistStart + 1); address allowedAddr = merkleHelper.getAllowedAddress(); @@ -252,7 +252,7 @@ contract ERC721MagicDropCloneableTest is Test { function testBurnHappyPath() public { // Public mint first - vm.warp(initialStartTime + 1); + vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.prank(user); @@ -276,7 +276,7 @@ contract ERC721MagicDropCloneableTest is Test { function testBurnNotOwnerReverts() public { // mint to user - vm.warp(initialStartTime + 1); + vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.prank(user); @@ -294,15 +294,15 @@ contract ERC721MagicDropCloneableTest is Test { function testGetPublicStage() public { PublicStage memory ps = token.getPublicStage(); - assertEq(ps.startTime, initialStartTime); - assertEq(ps.endTime, initialEndTime); + assertEq(ps.startTime, publicStart); + assertEq(ps.endTime, publicEnd); assertEq(ps.price, 0.01 ether); } function testGetAllowlistStage() public { AllowlistStage memory als = token.getAllowlistStage(); - assertEq(als.startTime, initialEndTime + 100); - assertEq(als.endTime, initialEndTime + 200); + assertEq(als.startTime, allowlistStart); + assertEq(als.endTime, allowlistEnd); assertEq(als.price, 0.005 ether); assertEq(als.merkleRoot, merkleHelper.getRoot()); } @@ -350,11 +350,11 @@ contract ERC721MagicDropCloneableTest is Test { } function testSetPublicStageOverlapWithAllowlistReverts() public { - // Current allowlist starts at initialEndTime+100 + // Current allowlist starts at publicEnd+100 // Try to set public stage that ends after that PublicStage memory overlappingStage = PublicStage({ startTime: uint64(block.timestamp + 10), - endTime: uint64(initialEndTime + 150), + endTime: uint64(allowlistEnd + 150), price: 0.01 ether }); @@ -364,11 +364,11 @@ contract ERC721MagicDropCloneableTest is Test { } function testSetAllowlistStageOverlapWithPublicReverts() public { - // Current public ends at initialEndTime + // Current public ends at publicEnd // Try to set allowlist that ends before public ends AllowlistStage memory overlappingStage = AllowlistStage({ - startTime: uint64(initialEndTime - 50), - endTime: uint64(initialEndTime + 10), + startTime: uint64(publicEnd - 50), + endTime: uint64(publicEnd + 10), price: 0.005 ether, merkleRoot: merkleHelper.getRoot() }); @@ -384,26 +384,55 @@ contract ERC721MagicDropCloneableTest is Test { assertEq(token.payoutRecipient(), address(0x8888)); } - function testWithdraw() public { - // Mint some tokens to generate balance - vm.warp(initialStartTime + 1); + /*============================================================== + = TEST SPLIT PROCEEDS = + ==============================================================*/ + + function testSplitProceeds() public { + // Move to public sale time + vm.warp(publicStart + 1); + + // Fund the user with enough ETH vm.deal(user, 1 ether); + // Check initial balances + uint256 initialProtocolBalance = token.PROTOCOL_FEE_RECIPIENT().balance; + uint256 initialPayoutBalance = payoutRecipient.balance; + + // User mints a token vm.prank(user); - token.mintPublic{value: 0.05 ether}(5, user); // 5 * 0.01 = 0.05 ether + token.mintPublic{value: 0.01 ether}(1, user); + + // Check balances after minting + uint256 expectedProtocolFee = (0.01 ether * token.PROTOCOL_FEE_BPS()) / token.BPS_DENOMINATOR(); + uint256 expectedPayout = 0.01 ether - expectedProtocolFee; + + assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance + expectedProtocolFee); + assertEq(payoutRecipient.balance, initialPayoutBalance + expectedPayout); + } - uint256 balanceBefore = payoutRecipient.balance; - uint256 contractBal = address(token).balance; - assertEq(contractBal, 0.05 ether); + function testSplitProceedsWithZeroPrice() public { + // Check initial balances + uint256 initialProtocolBalance = token.PROTOCOL_FEE_RECIPIENT().balance; + uint256 initialPayoutBalance = payoutRecipient.balance; vm.prank(owner); - token.withdraw(); + token.setPublicStage(PublicStage({ + startTime: uint64(publicStart), + endTime: uint64(publicEnd), + price: 0 + })); + + // Move to public sale time + vm.warp(publicStart + 1); + + // User mints a token with price 0 + vm.prank(user); + token.mintPublic{value: 0 ether}(1, user); - uint256 balanceAfter = payoutRecipient.balance; - // Protocol fee = 5% = 0.00025 ether - // Remaining to payout = 0.0475 ether - assertEq(balanceAfter - balanceBefore, 0.0475 ether); - assertEq(address(token).balance, 0); + // Check balances after minting + assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance); + assertEq(payoutRecipient.balance, initialPayoutBalance); } /*============================================================== @@ -418,7 +447,7 @@ contract ERC721MagicDropCloneableTest is Test { function testTokenURI() public { // Mint token #1 - vm.warp(initialStartTime + 1); + vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.prank(user); token.mintPublic{value: 0.01 ether}(1, user); From d6387cf78436719466a25f884c95bdb2523f46fc Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 12 Dec 2024 14:10:41 -0500 Subject: [PATCH 15/26] updates Signed-off-by: Adam Wolf --- .../common/interfaces/IMagicDropMetadata.sol | 85 ++++++++++++ .../clones/ERC721MagicDropCloneable.sol | 27 +--- .../ERC721MagicDropMetadataCloneable.sol | 42 +----- contracts/nft/erc721m/clones/Types.sol | 4 - .../interfaces/IERC721MagicDropMetadata.sol | 126 +++--------------- .../clones/ERC721MagicDropCloneable.t.sol | 28 +--- .../ERC721MagicDropMetadataCloneable.t.sol | 59 ++------ 7 files changed, 129 insertions(+), 242 deletions(-) create mode 100644 contracts/common/interfaces/IMagicDropMetadata.sol diff --git a/contracts/common/interfaces/IMagicDropMetadata.sol b/contracts/common/interfaces/IMagicDropMetadata.sol new file mode 100644 index 0000000..2f2330a --- /dev/null +++ b/contracts/common/interfaces/IMagicDropMetadata.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +interface IMagicDropMetadata { + /*============================================================== + = EVENTS = + ==============================================================*/ + + /// @notice Emitted when the contract URI is updated. + /// @param _contractURI The new contract URI. + event ContractURIUpdated(string _contractURI); + + /// @notice Emitted when the royalty info is updated. + /// @param receiver The new royalty receiver. + /// @param bps The new royalty basis points. + event RoyaltyInfoUpdated(address receiver, uint256 bps); + + /// @notice Emitted when the metadata is updated. (EIP-4906) + /// @param _fromTokenId The starting token ID. + /// @param _toTokenId The ending token ID. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + + /// @notice Emitted once when the token contract is deployed and initialized. + event MagicDropTokenDeployed(); + + /*============================================================== + = ERRORS = + ==============================================================*/ + + /// @notice Throw when the max supply is exceeded. + error CannotExceedMaxSupply(); + + /// @notice Throw when the max supply is less than the current supply. + error MaxSupplyCannotBeLessThanCurrentSupply(); + + /// @notice Throw when trying to increase the max supply. + error MaxSupplyCannotBeIncreased(); + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Returns the base URI used to construct token URIs + /// @dev This is concatenated with the token ID to form the complete token URI + /// @return The base URI string that prefixes all token URIs + function baseURI() external view returns (string memory); + + /// @notice Returns the contract-level metadata URI + /// @dev Used by marketplaces like MagicEden to display collection information + /// @return The URI string pointing to the contract's metadata JSON + function contractURI() external view returns (string memory); + + /// @notice Returns the address that receives royalty payments + /// @dev Used in conjunction with royaltyBps for EIP-2981 royalty standard + /// @return The address designated to receive royalty payments + function royaltyAddress() external view returns (address); + + /// @notice Returns the royalty percentage in basis points (1/100th of a percent) + /// @dev 100 basis points = 1%. Used in EIP-2981 royalty calculations + /// @return The royalty percentage in basis points (e.g., 250 = 2.5%) + function royaltyBps() external view returns (uint256); + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Sets the base URI for all token metadata + /// @dev This is a critical function that determines where all token metadata is hosted + /// Changing this will update the metadata location for all tokens in the collection + /// @param baseURI The new base URI string that will prefix all token URIs + function setBaseURI(string calldata baseURI) external; + + /// @notice Sets the contract-level metadata URI + /// @dev This metadata is used by marketplaces to display collection information + /// Should point to a JSON file following collection metadata standards + /// @param contractURI The new URI string pointing to the contract's metadata JSON + function setContractURI(string calldata contractURI) external; + + /// @notice Updates the royalty configuration for the collection + /// @dev Implements EIP-2981 for NFT royalty standards + /// The bps (basis points) must be between 0 and 10000 (0% to 100%) + /// @param newReceiver The address that will receive future royalty payments + /// @param newBps The royalty percentage in basis points (e.g., 250 = 2.5%) + function setRoyaltyInfo(address newReceiver, uint96 newBps) external; +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index aace594..5b93832 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -46,17 +46,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// @notice Only addresses proven by a valid Merkle proof can mint during this stage. AllowlistStage private _allowlistStage; - /*============================================================== - = EVENTS = - ==============================================================*/ - - /// @notice Emitted once when the token contract is deployed and initialized. - event MagicDropTokenDeployed(); - - /// @notice Emitted upon a successful withdrawal of funds. - /// @param amount The total amount of ETH withdrawn (including protocol fee). - event Withdraw(uint256 amount); - /*============================================================== = ERRORS = ==============================================================*/ @@ -70,12 +59,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// @notice Thrown when the provided ETH value for a mint is insufficient. error NotEnoughValue(); - /// @notice Thrown when a mint would exceed the wallet-specific minting limit. - error WalletLimitExceeded(); - - /// @notice Thrown when a withdrawal call fails to transfer funds. - error WithdrawFailed(); - /// @notice Thrown when the provided Merkle proof for an allowlist mint is invalid. error InvalidProof(); @@ -103,8 +86,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc function initialize(string memory _name, string memory _symbol, address _owner) public initializer { __ERC721ACloneable__init(_name, _symbol); __ERC721MagicDropMetadataCloneable__init(_owner); - - emit MagicDropTokenDeployed(); } /*============================================================== @@ -217,7 +198,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc ==============================================================*/ /// @notice Sets up the contract parameters in a single call. - /// @dev Only callable by the owner. Configures max supply, wallet limit, URIs, stages, payout recipient, and provenance. + /// @dev Only callable by the owner. Configures max supply, wallet limit, URIs, stages, payout recipient. /// @param config A struct containing all setup parameters. function setup(SetupConfig calldata config) external onlyOwner { if (config.maxSupply > 0) { @@ -247,10 +228,6 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc if (config.payoutRecipient != address(0)) { _setPayoutRecipient(config.payoutRecipient); } - - if (config.provenanceHash != bytes32(0)) { - _setProvenanceHash(config.provenanceHash); - } } /// @notice Sets the configuration of the public mint stage. @@ -398,4 +375,4 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc function _startTokenId() internal view virtual override returns (uint256) { return 1; } -} +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index aca7b46..8ebc0e4 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -12,9 +12,8 @@ import {ERC721AQueryableCloneable} from "./ERC721AQueryableCloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; /// @title ERC721MagicDropMetadataCloneable -/// @notice A cloneable ERC-721A implementation that supports adjustable metadata URIs, royalty configuration, -/// and optional provenance hashing for metadata integrity. Inherits conduit-based preapprovals, -/// making distribution more gas-efficient. +/// @notice A cloneable ERC-721A implementation that supports adjustable metadata URIs, royalty configuration. +/// Inherits conduit-based preapprovals, making distribution more gas-efficient. contract ERC721MagicDropMetadataCloneable is ERC721AConduitPreapprovedCloneable, IERC721MagicDropMetadata, @@ -30,6 +29,8 @@ contract ERC721MagicDropMetadataCloneable is /// @param owner The address of the contract owner. function __ERC721MagicDropMetadataCloneable__init(address owner) internal onlyInitializing { _initializeOwner(owner); + + emit MagicDropTokenDeployed(); } /*============================================================== @@ -52,11 +53,6 @@ contract ERC721MagicDropMetadataCloneable is /// @notice The per-wallet minting limit, restricting how many tokens a single address can mint. uint256 private _walletLimit; - /// @notice A provenance hash ensuring metadata integrity and fair distribution. - /// @dev Once tokens are minted, this value cannot be changed. Commonly used to verify that - /// the metadata ordering has not been manipulated post-reveal. - bytes32 private _provenanceHash; - /// @notice The address receiving royalty payments. address private _royaltyReceiver; @@ -78,7 +74,6 @@ contract ERC721MagicDropMetadataCloneable is function contractURI() public view override returns (string memory) { return _contractURI; } - /// @notice The maximum number of tokens that can ever be minted by this contract. /// @return The maximum supply of tokens. function maxSupply() public view returns (uint256) { @@ -91,12 +86,6 @@ contract ERC721MagicDropMetadataCloneable is return _walletLimit; } - /// @notice The assigned provenance hash used to ensure the integrity of the metadata ordering. - /// @return The provenance hash. - function provenanceHash() public view returns (bytes32) { - return _provenanceHash; - } - /// @notice The address designated to receive royalty payments on secondary sales. /// @return The royalty receiver address. function royaltyAddress() public view returns (address) { @@ -157,13 +146,6 @@ contract ERC721MagicDropMetadataCloneable is _setWalletLimit(newWalletLimit); } - /// @notice Sets the provenance hash, used to verify metadata integrity and prevent tampering. - /// @dev Can only be set before any tokens are minted. - /// @param newProvenanceHash The new provenance hash. - function setProvenanceHash(bytes32 newProvenanceHash) external onlyOwner { - _setProvenanceHash(newProvenanceHash); - } - /// @notice Configures the royalty information for secondary sales. /// @dev Sets a new receiver and basis points for royalties. Basis points define the percentage rate. /// @param newReceiver The address to receive royalties. @@ -189,7 +171,7 @@ contract ERC721MagicDropMetadataCloneable is function _baseURI() internal view override returns (string memory) { return _tokenBaseURI; } - + /// @notice Internal function setting the base URI for token metadata. /// @param newBaseURI The new base URI string. function _setBaseURI(string calldata newBaseURI) internal { @@ -224,18 +206,6 @@ contract ERC721MagicDropMetadataCloneable is emit WalletLimitUpdated(newWalletLimit); } - /// @notice Internal function setting the provenance hash. - /// @param newProvenanceHash The new provenance hash. - function _setProvenanceHash(bytes32 newProvenanceHash) internal { - if (_totalMinted() > 0) { - revert ProvenanceHashCannotBeUpdated(); - } - - bytes32 oldProvenanceHash = _provenanceHash; - _provenanceHash = newProvenanceHash; - emit ProvenanceHashUpdated(oldProvenanceHash, newProvenanceHash); - } - /// @notice Internal function setting the royalty information. /// @param newReceiver The address to receive royalties. /// @param newBps The royalty rate in basis points (e.g., 100 = 1%). @@ -252,4 +222,4 @@ contract ERC721MagicDropMetadataCloneable is _contractURI = newContractURI; emit ContractURIUpdated(newContractURI); } -} +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/Types.sol b/contracts/nft/erc721m/clones/Types.sol index 31fcf45..c1a7a97 100644 --- a/contracts/nft/erc721m/clones/Types.sol +++ b/contracts/nft/erc721m/clones/Types.sol @@ -40,8 +40,4 @@ struct SetupConfig { AllowlistStage allowlistStage; /// @dev The payout recipient of the token. address payoutRecipient; - /// @dev The provenance hash of the token. - /// @notice This is used to ensure the metadata is not tampered with. - /// A value of 0 is used to indicate that the provenance hash is not set. - bytes32 provenanceHash; } diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol index 1286a32..bdcbab6 100644 --- a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol +++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol @@ -1,114 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; -interface IERC721MagicDropMetadata { - /*============================================================== - = EVENTS = - ==============================================================*/ - - /// @notice Emitted when the contract URI is updated. - /// @param _contractURI The new contract URI. - event ContractURIUpdated(string _contractURI); - - /// @notice Emitted when the max supply is updated. - /// @param _maxSupply The new max supply. - event MaxSupplyUpdated(uint256 _maxSupply); +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; +interface IERC721MagicDropMetadata is IMagicDropMetadata { /// @notice Emitted when the wallet limit is updated. /// @param _walletLimit The new wallet limit. event WalletLimitUpdated(uint256 _walletLimit); - /// @notice Emitted when the provenance hash is updated. - /// @param oldHash The old provenance hash. - /// @param newHash The new provenance hash. - event ProvenanceHashUpdated(bytes32 oldHash, bytes32 newHash); - - /// @notice Emitted when the royalty info is updated. - /// @param receiver The new royalty receiver. - /// @param bps The new royalty basis points. - event RoyaltyInfoUpdated(address receiver, uint256 bps); - - /// @notice Emitted when the metadata is updated. (EIP-4906) - /// @param _fromTokenId The starting token ID. - /// @param _toTokenId The ending token ID. - event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); - - /*============================================================== - = ERRORS = - ==============================================================*/ - - /// @notice Throw when the provenance hash cannot be updated. - error ProvenanceHashCannotBeUpdated(); - - /// @notice Throw when the max supply is exceeded. - error CannotExceedMaxSupply(); - - /// @notice Throw when the max supply is less than the current supply. - error MaxSupplyCannotBeLessThanCurrentSupply(); - - /// @notice Throw when trying to increase the max supply. - error MaxSupplyCannotBeIncreased(); - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @notice Returns the base URI used to construct token URIs - /// @dev This is concatenated with the token ID to form the complete token URI - /// @return The base URI string that prefixes all token URIs - function baseURI() external view returns (string memory); - - /// @notice Returns the contract-level metadata URI - /// @dev Used by marketplaces like MagicEden to display collection information - /// @return The URI string pointing to the contract's metadata JSON - function contractURI() external view returns (string memory); + /// @notice Emitted when the max supply is updated. + /// @param newMaxSupply The new max supply. + event MaxSupplyUpdated(uint256 newMaxSupply); - /// @notice Returns the maximum number of tokens that can be minted - /// @dev This value cannot be increased once set, only decreased - /// @return The maximum supply cap for the collection - function maxSupply() external view returns (uint256); + /// @notice Thrown when a mint would exceed the wallet-specific minting limit. + error WalletLimitExceeded(); /// @notice Returns the maximum number of tokens that can be minted per wallet /// @dev Used to prevent excessive concentration of tokens in single wallets /// @return The maximum number of tokens allowed per wallet address function walletLimit() external view returns (uint256); - /// @notice Returns the provenance hash for the collection - /// @dev Used to prove that the token metadata/artwork hasn't been changed after mint - /// @return The 32-byte provenance hash of the collection - function provenanceHash() external view returns (bytes32); - - /// @notice Returns the address that receives royalty payments - /// @dev Used in conjunction with royaltyBps for EIP-2981 royalty standard - /// @return The address designated to receive royalty payments - function royaltyAddress() external view returns (address); - - /// @notice Returns the royalty percentage in basis points (1/100th of a percent) - /// @dev 100 basis points = 1%. Used in EIP-2981 royalty calculations - /// @return The royalty percentage in basis points (e.g., 250 = 2.5%) - function royaltyBps() external view returns (uint256); - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @notice Sets the base URI for all token metadata - /// @dev This is a critical function that determines where all token metadata is hosted - /// Changing this will update the metadata location for all tokens in the collection - /// @param baseURI The new base URI string that will prefix all token URIs - function setBaseURI(string calldata baseURI) external; - - /// @notice Sets the contract-level metadata URI - /// @dev This metadata is used by marketplaces to display collection information - /// Should point to a JSON file following collection metadata standards - /// @param contractURI The new URI string pointing to the contract's metadata JSON - function setContractURI(string calldata contractURI) external; - - /// @notice Updates the maximum supply cap for the collection - /// @dev Can only decrease the max supply, never increase it - /// Must be greater than or equal to the current total supply - /// @param maxSupply The new maximum number of tokens that can be minted - function setMaxSupply(uint256 maxSupply) external; + /// @notice Returns the maximum number of tokens that can be minted + /// @dev This value cannot be increased once set, only decreased + /// @return The maximum supply cap for the collection + function maxSupply() external view returns (uint256); /// @notice Updates the per-wallet token holding limit /// @dev Used to prevent token concentration and ensure fair distribution @@ -116,16 +31,9 @@ interface IERC721MagicDropMetadata { /// @param walletLimit The new maximum number of tokens allowed per wallet function setWalletLimit(uint256 walletLimit) external; - /// @notice Sets the provenance hash for the collection - /// @dev Should only be called once before the collection is revealed - /// Used to verify the integrity of the artwork/metadata after reveal - /// @param provenanceHash The 32-byte hash representing the collection's provenance - function setProvenanceHash(bytes32 provenanceHash) external; - - /// @notice Updates the royalty configuration for the collection - /// @dev Implements EIP-2981 for NFT royalty standards - /// The bps (basis points) must be between 0 and 10000 (0% to 100%) - /// @param newReceiver The address that will receive future royalty payments - /// @param newBps The royalty percentage in basis points (e.g., 250 = 2.5%) - function setRoyaltyInfo(address newReceiver, uint96 newBps) external; -} + /// @notice Updates the maximum supply cap for the collection + /// @dev Can only decrease the max supply, never increase it + /// Must be greater than or equal to the current total supply + /// @param maxSupply The new maximum number of tokens that can be minted + function setMaxSupply(uint256 maxSupply) external; +} \ No newline at end of file diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 0ca0229..c1c1ff2 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -8,6 +8,7 @@ import {LibClone} from "solady/src/utils/LibClone.sol"; import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; import {ERC721MagicDropCloneable} from "contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol"; +import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "contracts/nft/erc721m/clones/Types.sol"; import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; @@ -84,13 +85,8 @@ contract ERC721MagicDropCloneableTest is Test { price: 0.005 ether, merkleRoot: merkleHelper.getRoot() }), - publicStage: PublicStage({ - startTime: uint64(publicStart), - endTime: uint64(publicEnd), - price: 0.01 ether - }), - payoutRecipient: payoutRecipient, - provenanceHash: keccak256("some-provenance") + publicStage: PublicStage({startTime: uint64(publicStart), endTime: uint64(publicEnd), price: 0.01 ether}), + payoutRecipient: payoutRecipient }); vm.prank(owner); @@ -169,7 +165,7 @@ contract ERC721MagicDropCloneableTest is Test { assertEq(token.balanceOf(user), 5); // Attempt to mint one more - vm.expectRevert(ERC721MagicDropCloneable.WalletLimitExceeded.selector); + vm.expectRevert(IERC721MagicDropMetadata.WalletLimitExceeded.selector); token.mintPublic{value: 0.01 ether}(1, user); vm.stopPrank(); } @@ -241,7 +237,7 @@ contract ERC721MagicDropCloneableTest is Test { token.mintAllowlist{value: 0.025 ether}(5, allowedAddr, proof); assertEq(token.balanceOf(allowedAddr), 5); - vm.expectRevert(ERC721MagicDropCloneable.WalletLimitExceeded.selector); + vm.expectRevert(IERC721MagicDropMetadata.WalletLimitExceeded.selector); token.mintAllowlist{value: 0.005 ether}(1, allowedAddr, proof); vm.stopPrank(); } @@ -417,11 +413,7 @@ contract ERC721MagicDropCloneableTest is Test { uint256 initialPayoutBalance = payoutRecipient.balance; vm.prank(owner); - token.setPublicStage(PublicStage({ - startTime: uint64(publicStart), - endTime: uint64(publicEnd), - price: 0 - })); + token.setPublicStage(PublicStage({startTime: uint64(publicStart), endTime: uint64(publicEnd), price: 0})); // Move to public sale time vm.warp(publicStart + 1); @@ -439,12 +431,6 @@ contract ERC721MagicDropCloneableTest is Test { = METADATA = ==============================================================*/ - function testContractNameAndVersion() public { - (string memory n, string memory v) = token.contractNameAndVersion(); - assertEq(n, "ERC721MagicDropCloneable"); - assertEq(v, "1.0.0"); - } - function testTokenURI() public { // Mint token #1 vm.warp(publicStart + 1); @@ -459,4 +445,4 @@ contract ERC721MagicDropCloneableTest is Test { vm.expectRevert(); token.tokenURI(9999); } -} +} \ No newline at end of file diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol index 6cbee54..83be9c3 100644 --- a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol @@ -9,6 +9,7 @@ import {LibClone} from "solady/src/utils/LibClone.sol"; import {ERC721MagicDropMetadataCloneable} from "contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol"; import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; interface IERC2981 { function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address, uint256); @@ -43,11 +44,10 @@ contract ERC721MagicDropMetadataCloneableTest is Test { = INITIALIZATION = ==============================================================*/ - function testInitialization() public { + function testInitialization() public view { assertEq(token.owner(), owner); assertEq(token.maxSupply(), 0); assertEq(token.walletLimit(), 0); - assertEq(token.provenanceHash(), bytes32(0)); assertEq(token.baseURI(), ""); assertEq(token.contractURI(), ""); assertEq(token.royaltyAddress(), address(0)); @@ -86,7 +86,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { vm.startPrank(owner); token.mintForTest(user, 5); // now totalSupply = 5 vm.expectEmit(true, true, true, true); - emit IERC721MagicDropMetadata.BatchMetadataUpdate(0, 4); + emit IMagicDropMetadata.BatchMetadataUpdate(0, 4); token.setBaseURI("https://example.com/metadata/"); vm.stopPrank(); @@ -100,7 +100,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { function testSetContractURI() public { vm.prank(owner); vm.expectEmit(false, false, false, true); - emit IERC721MagicDropMetadata.ContractURIUpdated("https://new-contract-uri.json"); + emit IMagicDropMetadata.ContractURIUpdated("https://new-contract-uri.json"); token.setContractURI("https://new-contract-uri.json"); assertEq(token.contractURI(), "https://new-contract-uri.json"); } @@ -108,7 +108,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { function testSetEmptyContractURI() public { vm.prank(owner); vm.expectEmit(false, false, false, true); - emit IERC721MagicDropMetadata.ContractURIUpdated(""); + emit IMagicDropMetadata.ContractURIUpdated(""); token.setContractURI(""); assertEq(token.contractURI(), ""); } @@ -129,7 +129,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { vm.startPrank(owner); token.mintForTest(user, 10); // Currently minted = 10 - vm.expectRevert(IERC721MagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); token.setMaxSupply(5); // Setting exactly to 10 should pass @@ -140,7 +140,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { function testSetMaxSupplyCannotIncreaseBeyondOriginal() public { vm.startPrank(owner); token.setMaxSupply(1000); - vm.expectRevert(IERC721MagicDropMetadata.MaxSupplyCannotBeIncreased.selector); + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeIncreased.selector); token.setMaxSupply(2000); } @@ -164,27 +164,6 @@ contract ERC721MagicDropMetadataCloneableTest is Test { assertEq(token.walletLimit(), 0); } - /*============================================================== - = PROVENANCE HASH = - ==============================================================*/ - - function testSetProvenanceHashBeforeMint() public { - vm.prank(owner); - bytes32 ph = keccak256("test-provenance"); - vm.expectEmit(false, false, false, true); - emit IERC721MagicDropMetadata.ProvenanceHashUpdated(bytes32(0), ph); - token.setProvenanceHash(ph); - assertEq(token.provenanceHash(), ph); - } - - function testSetProvenanceHashAfterMintReverts() public { - vm.startPrank(owner); - token.mintForTest(user, 1); // now minted = 1 - bytes32 ph = keccak256("another-ph"); - vm.expectRevert(IERC721MagicDropMetadata.ProvenanceHashCannotBeUpdated.selector); - token.setProvenanceHash(ph); - } - /*============================================================== = ROYALTY INFO = ==============================================================*/ @@ -192,7 +171,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { function testSetRoyaltyInfo() public { vm.prank(owner); vm.expectEmit(false, false, false, true); - emit IERC721MagicDropMetadata.RoyaltyInfoUpdated(royaltyReceiver, 500); + emit IMagicDropMetadata.RoyaltyInfoUpdated(royaltyReceiver, 500); token.setRoyaltyInfo(royaltyReceiver, 500); assertEq(token.royaltyAddress(), royaltyReceiver); @@ -221,7 +200,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { token.mintForTest(user, 10); vm.expectEmit(true, true, true, true); - emit IERC721MagicDropMetadata.BatchMetadataUpdate(2, 5); + emit IMagicDropMetadata.BatchMetadataUpdate(2, 5); token.emitBatchMetadataUpdate(2, 5); vm.stopPrank(); } @@ -230,7 +209,7 @@ contract ERC721MagicDropMetadataCloneableTest is Test { = SUPPORTS INTERFACE = ==============================================================*/ - function testSupportsInterface() public { + function testSupportsInterface() public view { // ERC2981 interfaceId = 0x2a55205a assertTrue(token.supportsInterface(0x2a55205a)); // ERC4906 interfaceId = 0x49064906 @@ -247,24 +226,10 @@ contract ERC721MagicDropMetadataCloneableTest is Test { function testCannotSetMaxSupplyLessThanMintedEvenIfNotSetBefore() public { vm.startPrank(owner); token.mintForTest(user, 5); - vm.expectRevert(IERC721MagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); token.setMaxSupply(1); } - function testSettingProvenanceHashTwiceBeforeMintIsAllowed() public { - vm.startPrank(owner); - bytes32 ph1 = keccak256("ph1"); - token.setProvenanceHash(ph1); - assertEq(token.provenanceHash(), ph1); - - // Setting again before mint (though unusual) is possible since no tokens minted yet - bytes32 ph2 = keccak256("ph2"); - vm.expectEmit(false, false, false, true); - emit IERC721MagicDropMetadata.ProvenanceHashUpdated(ph1, ph2); - token.setProvenanceHash(ph2); - assertEq(token.provenanceHash(), ph2); - } - function testSetBaseURIEmptyString() public { vm.prank(owner); token.setBaseURI(""); @@ -277,4 +242,4 @@ contract ERC721MagicDropMetadataCloneableTest is Test { token.setMaxSupply(10); assertEq(token.maxSupply(), 10); } -} +} \ No newline at end of file From 96e5a6bf09733f4faa33a2648814cdd06e7c9d7f Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:20:10 -0500 Subject: [PATCH 16/26] updates Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.sol | 8 ++-- .../ERC721MagicDropMetadataCloneable.sol | 5 ++- .../clones/ERC721MagicDropCloneable.t.sol | 38 +++++++++---------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 5b93832..ae91047 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -95,9 +95,9 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// @notice Mints tokens during the public stage. /// @dev Requires that the current time is within the configured public stage interval. /// Reverts if the buyer does not send enough ETH, or if the wallet limit would be exceeded. - /// @param qty The number of tokens to mint. /// @param to The recipient address for the minted tokens. - function mintPublic(uint256 qty, address to) external payable { + /// @param qty The number of tokens to mint. + function mintPublic(address to, uint256 qty) external payable nonReentrant { PublicStage memory stage = _publicStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); @@ -122,10 +122,10 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// @notice Mints tokens during the allowlist stage. /// @dev Requires a valid Merkle proof and the current time within the allowlist stage interval. /// Reverts if the buyer sends insufficient ETH or if the wallet limit is exceeded. - /// @param qty The number of tokens to mint. /// @param to The recipient address for the minted tokens. + /// @param qty The number of tokens to mint. /// @param proof The Merkle proof verifying `to` is eligible for the allowlist. - function mintAllowlist(uint256 qty, address to, bytes32[] calldata proof) external payable { + function mintAllowlist(address to, uint256 qty, bytes32[] calldata proof) external payable { AllowlistStage memory stage = _allowlistStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert AllowlistStageNotActive(); diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 8ebc0e4..8062c15 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -66,7 +66,7 @@ contract ERC721MagicDropMetadataCloneable is /// @notice Returns the current base URI used to construct token URIs. /// @return The base URI as a string. function baseURI() public view override returns (string memory) { - return _baseURI(); + return _tokenBaseURI; } /// @notice Returns a URI representing contract-level metadata, often used by marketplaces. @@ -74,6 +74,7 @@ contract ERC721MagicDropMetadataCloneable is function contractURI() public view override returns (string memory) { return _contractURI; } + /// @notice The maximum number of tokens that can ever be minted by this contract. /// @return The maximum supply of tokens. function maxSupply() public view returns (uint256) { @@ -171,7 +172,7 @@ contract ERC721MagicDropMetadataCloneable is function _baseURI() internal view override returns (string memory) { return _tokenBaseURI; } - + /// @notice Internal function setting the base URI for token metadata. /// @param newBaseURI The new base URI string. function _setBaseURI(string calldata newBaseURI) internal { diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index c1c1ff2..77b1bbf 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -121,7 +121,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.deal(user, 1 ether); vm.prank(user); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); assertEq(token.balanceOf(user), 1); } @@ -133,7 +133,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(user); vm.expectRevert(ERC721MagicDropCloneable.PublicStageNotActive.selector); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); } function testMintPublicAfterEndReverts() public { @@ -143,7 +143,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(user); vm.expectRevert(ERC721MagicDropCloneable.PublicStageNotActive.selector); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); } function testMintPublicNotEnoughValueReverts() public { @@ -152,7 +152,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(user); vm.expectRevert(ERC721MagicDropCloneable.NotEnoughValue.selector); - token.mintPublic{value: 0.005 ether}(1, user); + token.mintPublic{value: 0.005 ether}(user, 1); } function testMintPublicWalletLimitExceededReverts() public { @@ -161,12 +161,12 @@ contract ERC721MagicDropCloneableTest is Test { vm.startPrank(user); // Mint up to the limit (5) - token.mintPublic{value: 0.05 ether}(5, user); + token.mintPublic{value: 0.05 ether}(user, 5); assertEq(token.balanceOf(user), 5); // Attempt to mint one more vm.expectRevert(IERC721MagicDropMetadata.WalletLimitExceeded.selector); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); vm.stopPrank(); } @@ -181,7 +181,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.deal(merkleHelper.getAllowedAddress(), 1 ether); vm.prank(merkleHelper.getAllowedAddress()); token.mintAllowlist{value: 0.005 ether}( - 1, merkleHelper.getAllowedAddress(), merkleHelper.getProofFor(merkleHelper.getAllowedAddress()) + merkleHelper.getAllowedAddress(), 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()) ); assertEq(token.balanceOf(merkleHelper.getAllowedAddress()), 1); @@ -197,7 +197,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(allowedAddr); vm.expectRevert(ERC721MagicDropCloneable.InvalidProof.selector); - token.mintAllowlist{value: 0.005 ether}(1, user, proof); + token.mintAllowlist{value: 0.005 ether}(user, 1, proof); } function testMintAllowlistNotActiveReverts() public { @@ -210,7 +210,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(allowedAddr); vm.expectRevert(ERC721MagicDropCloneable.AllowlistStageNotActive.selector); - token.mintAllowlist{value: 0.005 ether}(1, allowedAddr, proof); + token.mintAllowlist{value: 0.005 ether}(allowedAddr, 1, proof); } function testMintAllowlistNotEnoughValueReverts() public { @@ -222,7 +222,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(allowedAddr); vm.expectRevert(ERC721MagicDropCloneable.NotEnoughValue.selector); - token.mintAllowlist{value: 0.001 ether}(1, allowedAddr, proof); + token.mintAllowlist{value: 0.001 ether}(allowedAddr, 1, proof); } function testMintAllowlistWalletLimitExceededReverts() public { @@ -234,11 +234,11 @@ contract ERC721MagicDropCloneableTest is Test { vm.startPrank(allowedAddr); // Mint up to the limit - token.mintAllowlist{value: 0.025 ether}(5, allowedAddr, proof); + token.mintAllowlist{value: 0.025 ether}(allowedAddr, 5, proof); assertEq(token.balanceOf(allowedAddr), 5); vm.expectRevert(IERC721MagicDropMetadata.WalletLimitExceeded.selector); - token.mintAllowlist{value: 0.005 ether}(1, allowedAddr, proof); + token.mintAllowlist{value: 0.005 ether}(allowedAddr, 1, proof); vm.stopPrank(); } @@ -252,7 +252,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.deal(user, 1 ether); vm.prank(user); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); uint256 tokenId = 1; assertEq(token.ownerOf(tokenId), user); @@ -276,7 +276,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.deal(user, 1 ether); vm.prank(user); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); uint256 tokenId = 1; vm.prank(user2); @@ -295,7 +295,7 @@ contract ERC721MagicDropCloneableTest is Test { assertEq(ps.price, 0.01 ether); } - function testGetAllowlistStage() public { + function testGetAllowlistStage() public view { AllowlistStage memory als = token.getAllowlistStage(); assertEq(als.startTime, allowlistStart); assertEq(als.endTime, allowlistEnd); @@ -311,7 +311,7 @@ contract ERC721MagicDropCloneableTest is Test { = SUPPORTSINTERFACE = ==============================================================*/ - function testSupportsInterface() public { + function testSupportsInterface() public view { // Just checks a known supported interface assertTrue(token.supportsInterface(type(IERC721MagicDropMetadata).interfaceId)); } @@ -397,7 +397,7 @@ contract ERC721MagicDropCloneableTest is Test { // User mints a token vm.prank(user); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); // Check balances after minting uint256 expectedProtocolFee = (0.01 ether * token.PROTOCOL_FEE_BPS()) / token.BPS_DENOMINATOR(); @@ -420,7 +420,7 @@ contract ERC721MagicDropCloneableTest is Test { // User mints a token with price 0 vm.prank(user); - token.mintPublic{value: 0 ether}(1, user); + token.mintPublic{value: 0 ether}(user, 1); // Check balances after minting assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance); @@ -436,7 +436,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.prank(user); - token.mintPublic{value: 0.01 ether}(1, user); + token.mintPublic{value: 0.01 ether}(user, 1); string memory uri = token.tokenURI(1); assertEq(uri, "https://example.com/metadata/1"); } From c3c2f023b8fe1ca8b027ba72ebe4ba92d9f3dc4d Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:21:59 -0500 Subject: [PATCH 17/26] add file end newline Signed-off-by: Adam Wolf --- contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol | 2 +- .../nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol | 2 +- test/erc721m/clones/ERC721MagicDropCloneable.t.sol | 2 +- test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index ae91047..a13b5a3 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -375,4 +375,4 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc function _startTokenId() internal view virtual override returns (uint256) { return 1; } -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 8062c15..39eb33c 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -223,4 +223,4 @@ contract ERC721MagicDropMetadataCloneable is _contractURI = newContractURI; emit ContractURIUpdated(newContractURI); } -} \ No newline at end of file +} diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 77b1bbf..44fa99f 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -445,4 +445,4 @@ contract ERC721MagicDropCloneableTest is Test { vm.expectRevert(); token.tokenURI(9999); } -} \ No newline at end of file +} diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol index 83be9c3..5a61d64 100644 --- a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol @@ -242,4 +242,4 @@ contract ERC721MagicDropMetadataCloneableTest is Test { token.setMaxSupply(10); assertEq(token.maxSupply(), 10); } -} \ No newline at end of file +} From e5ac06363cf8d18fa610eb11392dbf0893710605 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 13:01:25 -0500 Subject: [PATCH 18/26] updates Signed-off-by: Adam Wolf --- .../common/interfaces/IMagicDropMetadata.sol | 3 ++ .../clones/ERC721MagicDropCloneable.sol | 34 +++++++++---------- .../ERC721MagicDropMetadataCloneable.sol | 28 +++++++++------ .../interfaces/IERC721MagicDropMetadata.sol | 2 +- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/contracts/common/interfaces/IMagicDropMetadata.sol b/contracts/common/interfaces/IMagicDropMetadata.sol index 2f2330a..e62ef81 100644 --- a/contracts/common/interfaces/IMagicDropMetadata.sol +++ b/contracts/common/interfaces/IMagicDropMetadata.sol @@ -36,6 +36,9 @@ interface IMagicDropMetadata { /// @notice Throw when trying to increase the max supply. error MaxSupplyCannotBeIncreased(); + /// @notice Throw when the max supply is greater than 2^64. + error MaxSupplyCannotBeGreaterThan2ToThe64thPower(); + /*============================================================== = PUBLIC VIEW METHODS = ==============================================================*/ diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index a13b5a3..abea19a 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.22; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; @@ -17,7 +16,7 @@ import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; /// @notice A cloneable ERC-721A drop contract that supports both a public minting stage and an allowlist minting stage. /// @dev This contract extends metadata configuration, ownership, and royalty support from its parent, while adding /// time-gated, price-defined minting stages. It also incorporates a payout recipient and protocol fee structure. -contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, ReentrancyGuard { +contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { /*============================================================== = STORAGE = ==============================================================*/ @@ -97,7 +96,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// Reverts if the buyer does not send enough ETH, or if the wallet limit would be exceeded. /// @param to The recipient address for the minted tokens. /// @param qty The number of tokens to mint. - function mintPublic(address to, uint256 qty) external payable nonReentrant { + function mintPublic(address to, uint256 qty) external payable { PublicStage memory stage = _publicStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); @@ -108,15 +107,19 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert NotEnoughValue(); } - if (_numberMinted(to) + qty > this.walletLimit()) { + if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) { revert WalletLimitExceeded(); } - if (stage.price != 0) { - _splitProceeds(); + if (_totalMinted() + qty > _maxSupply) { + revert CannotExceedMaxSupply(); } _safeMint(to, qty); + + if (stage.price != 0) { + _splitProceeds(); + } } /// @notice Mints tokens during the allowlist stage. @@ -140,15 +143,19 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert NotEnoughValue(); } - if (_numberMinted(to) + qty > this.walletLimit()) { + if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) { revert WalletLimitExceeded(); } - if (stage.price != 0) { - _splitProceeds(); + if (_totalMinted() + qty > _maxSupply) { + revert CannotExceedMaxSupply(); } _safeMint(to, qty); + + if (stage.price != 0) { + _splitProceeds(); + } } /// @notice Burns a specific token. @@ -368,11 +375,4 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc function _guardInitializeOwner() internal pure virtual override returns (bool) { return true; } - - /// @notice Returns the token ID where enumeration starts. - /// @dev Overridden to start from token ID 1 instead of 0. - /// @return The first valid token ID. - function _startTokenId() internal view virtual override returns (uint256) { - return 1; - } -} +} \ No newline at end of file diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 39eb33c..143fa57 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; +import { IERC2981 } from "@openzeppelin/contracts/interfaces/IERC2981.sol"; + import {ERC2981} from "solady/src/tokens/ERC2981.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; @@ -39,25 +41,25 @@ contract ERC721MagicDropMetadataCloneable is /// @notice The base URI used to construct `tokenURI` results. /// @dev This value can be updated by the contract owner. Typically points to an off-chain IPFS/HTTPS endpoint. - string private _tokenBaseURI; + string internal _tokenBaseURI; /// @notice A URI providing contract-level metadata (e.g., for marketplaces). /// @dev Can be updated by the owner. Often returns metadata in a JSON format describing the project. - string private _contractURI; + string internal _contractURI; /// @notice The maximum total number of tokens that can ever be minted. /// @dev Acts as a cap on supply. Decreasing is allowed (if no tokens are over that limit), /// but increasing supply is forbidden after initialization. - uint256 private _maxSupply; + uint256 internal _maxSupply; /// @notice The per-wallet minting limit, restricting how many tokens a single address can mint. - uint256 private _walletLimit; + uint256 internal _walletLimit; /// @notice The address receiving royalty payments. - address private _royaltyReceiver; + address internal _royaltyReceiver; /// @notice The royalty amount (in basis points) for secondary sales (e.g., 100 = 1%). - uint96 private _royaltyBps; + uint96 internal _royaltyBps; /*============================================================== = PUBLIC VIEW METHODS = @@ -74,7 +76,7 @@ contract ERC721MagicDropMetadataCloneable is function contractURI() public view override returns (string memory) { return _contractURI; } - + /// @notice The maximum number of tokens that can ever be minted by this contract. /// @return The maximum supply of tokens. function maxSupply() public view returns (uint256) { @@ -107,12 +109,12 @@ contract ERC721MagicDropMetadataCloneable is public view virtual - override(ERC721ACloneable, IERC721A, ERC2981) + override(ERC2981, ERC721ACloneable, IERC721A) returns (bool) { return interfaceId == 0x2a55205a // ERC-2981 royalties || interfaceId == 0x49064906 // ERC-4906 metadata updates - || super.supportsInterface(interfaceId); + || ERC721ACloneable.supportsInterface(interfaceId); } /*============================================================== @@ -192,10 +194,14 @@ contract ERC721MagicDropMetadataCloneable is revert MaxSupplyCannotBeIncreased(); } - if (newMaxSupply < totalSupply()) { + if (newMaxSupply < _totalMinted()) { revert MaxSupplyCannotBeLessThanCurrentSupply(); } + if (newMaxSupply > 2 ** 64 - 1) { + revert MaxSupplyCannotBeGreaterThan2ToThe64thPower(); + } + _maxSupply = newMaxSupply; emit MaxSupplyUpdated(newMaxSupply); } @@ -223,4 +229,4 @@ contract ERC721MagicDropMetadataCloneable is _contractURI = newContractURI; emit ContractURIUpdated(newContractURI); } -} +} \ No newline at end of file diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol index bdcbab6..822c905 100644 --- a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol +++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol @@ -36,4 +36,4 @@ interface IERC721MagicDropMetadata is IMagicDropMetadata { /// Must be greater than or equal to the current total supply /// @param maxSupply The new maximum number of tokens that can be minted function setMaxSupply(uint256 maxSupply) external; -} \ No newline at end of file +} From fd3fc3b158c0f04611d15d7525b7dfd1404ef5d4 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 13:05:15 -0500 Subject: [PATCH 19/26] updates Signed-off-by: Adam Wolf --- .../common/interfaces/IMagicDropMetadata.sol | 2 +- .../clones/ERC721MagicDropCloneable.sol | 2 +- .../ERC721MagicDropMetadataCloneable.sol | 4 +- .../clones/ERC721MagicDropCloneable.t.sol | 122 ++++++++++++------ .../ERC721MagicDropMetadataCloneable.t.sol | 11 ++ test/helpers/MerkleTestHelper.sol | 37 ++++++ 6 files changed, 134 insertions(+), 44 deletions(-) create mode 100644 test/helpers/MerkleTestHelper.sol diff --git a/contracts/common/interfaces/IMagicDropMetadata.sol b/contracts/common/interfaces/IMagicDropMetadata.sol index e62ef81..5b5ba91 100644 --- a/contracts/common/interfaces/IMagicDropMetadata.sol +++ b/contracts/common/interfaces/IMagicDropMetadata.sol @@ -85,4 +85,4 @@ interface IMagicDropMetadata { /// @param newReceiver The address that will receive future royalty payments /// @param newBps The royalty percentage in basis points (e.g., 250 = 2.5%) function setRoyaltyInfo(address newReceiver, uint96 newBps) external; -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index abea19a..8605185 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -375,4 +375,4 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { function _guardInitializeOwner() internal pure virtual override returns (bool) { return true; } -} \ No newline at end of file +} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 143fa57..717aaa8 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; -import { IERC2981 } from "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; import {ERC2981} from "solady/src/tokens/ERC2981.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; @@ -229,4 +229,4 @@ contract ERC721MagicDropMetadataCloneable is _contractURI = newContractURI; emit ContractURIUpdated(newContractURI); } -} \ No newline at end of file +} diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 44fa99f..a188619 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -7,45 +7,13 @@ import {console2} from "forge-std/console2.sol"; import {LibClone} from "solady/src/utils/LibClone.sol"; import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {MerkleTestHelper} from "test/helpers/MerkleTestHelper.sol"; + import {ERC721MagicDropCloneable} from "contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol"; import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "contracts/nft/erc721m/clones/Types.sol"; import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; - -// Dummy merkle proof generation utilities for testing -contract MerkleTestHelper { - // This is a placeholder helper. In a real test, you'd generate a real merkle tree offline. - // Here we hardcode a single allowlisted address and its proof. - bytes32[] internal proof; - bytes32 internal root; - address internal allowedAddr; - - constructor() { - allowedAddr = address(0xABCD); - // For simplicity, root = keccak256(abi.encodePacked(allowedAddr)) - // Proof is empty since this is a single-leaf tree. - root = keccak256(abi.encodePacked(allowedAddr)); - } - - function getRoot() external view returns (bytes32) { - return root; - } - - function getProofFor(address addr) external view returns (bytes32[] memory) { - if (addr == allowedAddr) { - // Single-leaf tree: no proof necessary except empty array - return new bytes32[](0); - } else { - // No valid proof - bytes32[] memory emptyProof; - return emptyProof; - } - } - - function getAllowedAddress() external view returns (address) { - return allowedAddr; - } -} +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; contract ERC721MagicDropCloneableTest is Test { ERC721MagicDropCloneable public token; @@ -54,6 +22,7 @@ contract ERC721MagicDropCloneableTest is Test { address internal owner = address(0x1234); address internal user = address(0x1111); address internal user2 = address(0x2222); + address internal allowedAddr = address(0x3333); address internal payoutRecipient = address(0x9999); uint256 internal publicStart; uint256 internal publicEnd; @@ -62,7 +31,7 @@ contract ERC721MagicDropCloneableTest is Test { function setUp() public { token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable()))); - merkleHelper = new MerkleTestHelper(); + merkleHelper = new MerkleTestHelper(allowedAddr); // Initialize token token.initialize("TestToken", "TT", owner); @@ -170,6 +139,19 @@ contract ERC721MagicDropCloneableTest is Test { vm.stopPrank(); } + function testMintPublicMaxSupplyExceededReverts() public { + vm.warp(publicStart + 1); + vm.deal(user, 11 ether); + + vm.prank(owner); + // unlimited wallet limit for the purpose of this test + token.setWalletLimit(0); + + vm.prank(user); + vm.expectRevert(IMagicDropMetadata.CannotExceedMaxSupply.selector); + token.mintPublic{value: 11 ether}(user, 1001); + } + /*============================================================== = TEST ALLOWLIST MINTING STAGE = ==============================================================*/ @@ -242,6 +224,23 @@ contract ERC721MagicDropCloneableTest is Test { vm.stopPrank(); } + function testMintAllowlistMaxSupplyExceededReverts() public { + // Move time to allowlist + vm.warp(allowlistStart + 1); + + vm.prank(owner); + // unlimited wallet limit for the purpose of this test + token.setWalletLimit(0); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + vm.deal(allowedAddr, 11 ether); + + vm.prank(allowedAddr); + vm.expectRevert(IMagicDropMetadata.CannotExceedMaxSupply.selector); + token.mintAllowlist{value: 11 ether}(allowedAddr, 1001, proof); + } + /*============================================================== = BURNING = ==============================================================*/ @@ -254,7 +253,7 @@ contract ERC721MagicDropCloneableTest is Test { vm.prank(user); token.mintPublic{value: 0.01 ether}(user, 1); - uint256 tokenId = 1; + uint256 tokenId = 0; assertEq(token.ownerOf(tokenId), user); vm.prank(user); @@ -427,22 +426,65 @@ contract ERC721MagicDropCloneableTest is Test { assertEq(payoutRecipient.balance, initialPayoutBalance); } + function testSplitProceedsPayoutRecipientZeroAddressReverts() public { + // Move to public sale time + vm.warp(publicStart + 1); + + vm.prank(owner); + token.setPayoutRecipient(address(0)); + assertEq(token.payoutRecipient(), address(0)); + + vm.deal(user, 1 ether); + + vm.prank(user); + vm.expectRevert(ERC721MagicDropCloneable.PayoutRecipientCannotBeZeroAddress.selector); + token.mintPublic{value: 0.01 ether}(user, 1); + } + /*============================================================== = METADATA = ==============================================================*/ function testTokenURI() public { - // Mint token #1 vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.prank(user); token.mintPublic{value: 0.01 ether}(user, 1); - string memory uri = token.tokenURI(1); - assertEq(uri, "https://example.com/metadata/1"); + string memory uri = token.tokenURI(0); + assertEq(uri, "https://example.com/metadata/0"); + } + + function testTokenURIWithEmptyBaseURI() public { + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + vm.prank(user); + token.mintPublic{value: 0.01 ether}(user, 1); + + vm.prank(owner); + token.setBaseURI(""); + assertEq(token.tokenURI(0), ""); + } + + function testTokenURIWithoutTrailingSlash() public { + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + vm.prank(user); + token.mintPublic{value: 0.01 ether}(user, 1); + + vm.prank(owner); + token.setBaseURI("https://example.com/metadata"); + assertEq(token.tokenURI(0), "https://example.com/metadata"); } function testTokenURIForNonexistentTokenReverts() public { vm.expectRevert(); token.tokenURI(9999); } + + function testContractNameAndVersion() public { + (string memory name, string memory version) = token.contractNameAndVersion(); + // check that a value is returned + assert(bytes(name).length > 0); + assert(bytes(version).length > 0); + } } diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol index 5a61d64..779f95e 100644 --- a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol @@ -6,6 +6,7 @@ import {console2} from "forge-std/console2.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; import {LibClone} from "solady/src/utils/LibClone.sol"; +import {IERC721A} from "erc721a/contracts/IERC721A.sol"; import {ERC721MagicDropMetadataCloneable} from "contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol"; import {IERC721MagicDropMetadata} from "contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol"; @@ -214,6 +215,10 @@ contract ERC721MagicDropMetadataCloneableTest is Test { assertTrue(token.supportsInterface(0x2a55205a)); // ERC4906 interfaceId = 0x49064906 assertTrue(token.supportsInterface(0x49064906)); + // ERC721A interfaceId = 0x80ac58cd + assertTrue(token.supportsInterface(0x80ac58cd)); + // ERC721Metadata interfaceId = 0x5b5e139f + assertTrue(token.supportsInterface(0x5b5e139f)); // Some random interface assertFalse(token.supportsInterface(0x12345678)); } @@ -242,4 +247,10 @@ contract ERC721MagicDropMetadataCloneableTest is Test { token.setMaxSupply(10); assertEq(token.maxSupply(), 10); } + + function testMaxSupplyCannotBeGreaterThan2ToThe64thPower() public { + vm.startPrank(owner); + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeGreaterThan2ToThe64thPower.selector); + token.setMaxSupply(2 ** 64); + } } diff --git a/test/helpers/MerkleTestHelper.sol b/test/helpers/MerkleTestHelper.sol new file mode 100644 index 0000000..dfcfeca --- /dev/null +++ b/test/helpers/MerkleTestHelper.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +// Dummy merkle proof generation utilities for testing +contract MerkleTestHelper { + // This is a placeholder helper. In a real test, you'd generate a real merkle tree offline. + // Here we hardcode a single allowlisted address and its proof. + bytes32[] internal _proof; + bytes32 internal _root; + address internal _allowedAddr; + + constructor(address allowedAddr) { + _allowedAddr = allowedAddr; + // For simplicity, root = keccak256(abi.encodePacked(_allowedAddr)) + // Proof is empty since this is a single-leaf tree. + _root = keccak256(abi.encodePacked(_allowedAddr)); + } + + function getRoot() external view returns (bytes32) { + return _root; + } + + function getProofFor(address addr) external view returns (bytes32[] memory) { + if (addr == _allowedAddr) { + // Single-leaf tree: no proof necessary except empty array + return new bytes32[](0); + } else { + // No valid proof + bytes32[] memory emptyProof; + return emptyProof; + } + } + + function getAllowedAddress() external view returns (address) { + return _allowedAddr; + } +} From fb00eab71cd6c113c6ce4795583a248153549c6d Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 13:17:20 -0500 Subject: [PATCH 20/26] fix Signed-off-by: Adam Wolf --- contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 8605185..a154490 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -272,7 +272,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { // Ensure the public stage starts after the allowlist stage ends if (_allowlistStage.startTime != 0 && _allowlistStage.endTime != 0) { - if (stage.startTime < _allowlistStage.endTime) { + if (stage.startTime <= _allowlistStage.endTime) { revert InvalidPublicStageTime(); } } @@ -290,7 +290,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { // Ensure the public stage starts after the allowlist stage ends if (_publicStage.startTime != 0 && _publicStage.endTime != 0) { - if (stage.endTime > _publicStage.startTime) { + if (stage.endTime >= _publicStage.startTime) { revert InvalidAllowlistStageTime(); } } From 2185a94be7bca41e99cea7a0ad36d43281d16fed Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 19 Dec 2024 17:02:04 -0500 Subject: [PATCH 21/26] add royalties to config Signed-off-by: Adam Wolf --- contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol | 4 ++++ contracts/nft/erc721m/clones/Types.sol | 4 ++++ test/erc721m/clones/ERC721MagicDropCloneable.t.sol | 8 ++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index a154490..e2677fa 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -235,6 +235,10 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { if (config.payoutRecipient != address(0)) { _setPayoutRecipient(config.payoutRecipient); } + + if (config.royaltyRecipient != address(0)) { + _setRoyaltyInfo(config.royaltyRecipient, config.royaltyBps); + } } /// @notice Sets the configuration of the public mint stage. diff --git a/contracts/nft/erc721m/clones/Types.sol b/contracts/nft/erc721m/clones/Types.sol index c1a7a97..3c84934 100644 --- a/contracts/nft/erc721m/clones/Types.sol +++ b/contracts/nft/erc721m/clones/Types.sol @@ -40,4 +40,8 @@ struct SetupConfig { AllowlistStage allowlistStage; /// @dev The payout recipient of the token. address payoutRecipient; + /// @dev The royalty recipient of the token. + address royaltyRecipient; + /// @dev The royalty basis points of the token. + uint96 royaltyBps; } diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index a188619..87a9cfe 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -28,6 +28,9 @@ contract ERC721MagicDropCloneableTest is Test { uint256 internal publicEnd; uint256 internal allowlistStart; uint256 internal allowlistEnd; + address royaltyRecipient = address(0x8888); + uint96 royaltyBps = 1000; + function setUp() public { token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable()))); @@ -55,7 +58,9 @@ contract ERC721MagicDropCloneableTest is Test { merkleRoot: merkleHelper.getRoot() }), publicStage: PublicStage({startTime: uint64(publicStart), endTime: uint64(publicEnd), price: 0.01 ether}), - payoutRecipient: payoutRecipient + payoutRecipient: payoutRecipient, + royaltyRecipient: royaltyRecipient, + royaltyBps: royaltyBps }); vm.prank(owner); @@ -70,7 +75,6 @@ contract ERC721MagicDropCloneableTest is Test { assertEq(token.owner(), owner); assertEq(token.name(), "TestToken"); assertEq(token.symbol(), "TT"); - assertEq(token.payoutRecipient(), payoutRecipient); } function testMultipleInitializationReverts() public { From 1cca4f62e83ec83385fa928c70da8b2667ed2f6a Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 19 Dec 2024 17:14:57 -0500 Subject: [PATCH 22/26] fmt Signed-off-by: Adam Wolf --- test/erc721m/clones/ERC721MagicDropCloneable.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 87a9cfe..8671cb5 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -31,7 +31,6 @@ contract ERC721MagicDropCloneableTest is Test { address royaltyRecipient = address(0x8888); uint96 royaltyBps = 1000; - function setUp() public { token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable()))); merkleHelper = new MerkleTestHelper(allowedAddr); From 44f284939a76fdb43843a5bb2d6ed8af1f1d2887 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 2 Jan 2025 18:01:24 -0500 Subject: [PATCH 23/26] add config function Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index e2677fa..5883a08 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -169,6 +169,24 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { = PUBLIC VIEW METHODS = ==============================================================*/ + /// @notice Returns the current configuration of the contract. + /// @return The current configuration of the contract. + function getConfig() external view returns (SetupConfig memory) { + SetupConfig memory newConfig = SetupConfig({ + maxSupply: _maxSupply, + walletLimit: _walletLimit, + baseURI: _baseURI(), + contractURI: _contractURI, + allowlistStage: _allowlistStage, + publicStage: _publicStage, + payoutRecipient: _payoutRecipient, + royaltyRecipient: _royaltyReceiver, + royaltyBps: _royaltyBps + }); + + return newConfig; + } + /// @notice Returns the current public stage configuration (startTime, endTime, price). /// @return The current public stage settings. function getPublicStage() external view returns (PublicStage memory) { From c04f419b2cc81722100e22604e8e4773db1d4b0c Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 6 Jan 2025 17:50:33 -0500 Subject: [PATCH 24/26] emit contract uri event Signed-off-by: Adam Wolf --- .../nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 717aaa8..bbe1882 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -133,6 +133,8 @@ contract ERC721MagicDropMetadataCloneable is /// @param newContractURI The new contract metadata URI. function setContractURI(string calldata newContractURI) external override onlyOwner { _setContractURI(newContractURI); + + emit ContractURIUpdated(newContractURI); } /// @notice Adjusts the maximum token supply. From 763b1d5a1ce895022854b926fd7d7ffb8080f64c Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 7 Jan 2025 16:48:18 -0500 Subject: [PATCH 25/26] add heading Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.sol | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 5883a08..cc4f488 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -12,6 +12,52 @@ import {ERC721ACloneable} from "./ERC721ACloneable.sol"; import {IERC721MagicDropMetadata} from "../interfaces/IERC721MagicDropMetadata.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; +/// ........ +/// ..... .. ... +/// .. ..... .. .. +/// .. ... ..... .. .. +/// .. ...... .. ...... .. +/// .. ......... ......... .... +/// .... .. .. ... +/// ........ ......... .. +/// .. ... ... .. ......... +/// .. .......... .... .... ....... ........ +/// ....... .. .. ... .... ..... .. +/// ........ . ... .. .. +/// . ..... ........ .... .. +/// .. .. ... ........... ... ... +/// ....... .. ...... ... .. +/// ............ ... ........ .. .. +/// ... ..... .. .. .. .. .. ...... +/// .. ........ ... .. .. .. .... .... +/// ....... .. .. ...... ....... .. +/// .. ..... .. .... .. +/// .. .... ......... . .. .. +/// ... .... .. ......... . .. .. +/// .... .... .. ..... ...... ... +/// ..... .. ........ ... ... +/// ... .. .. .. ...... ..... .. +/// .. .... ... ... .. .. +/// .. .... .. .. .. +/// . ...... .. .. .. +/// .. ...................... .............. +/// .. ................ .... ... +/// . ... ........ +/// .. ... ...... .. +/// .. .... ...EMMY.... +/// .. .. ... .... .... .. +/// .. .. ..... .......... +/// ... .. ... ...... +/// ... .... .. .. +/// .. ..... ... +/// ..... .... ........ ... +/// ........ .. ..... .......... +/// .. ........ .. ..MAGIC..... . +/// .... .... .... ..EDEN.... +/// ..... . ... ...... +/// .. ....... .. +/// ..... ..... +/// .... /// @title ERC721MagicDropCloneable /// @notice A cloneable ERC-721A drop contract that supports both a public minting stage and an allowlist minting stage. /// @dev This contract extends metadata configuration, ownership, and royalty support from its parent, while adding From bcd2e660908b0ae0699dc98440d0cf3d03984fda Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 8 Jan 2025 17:59:13 -0500 Subject: [PATCH 26/26] zero protocol fee Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.sol | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index cc4f488..00bb18c 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -77,7 +77,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { /// @dev The protocol fee expressed in basis points (e.g., 500 = 5%). /// @notice This fee is taken from the contract's entire balance upon withdrawal. - uint256 public constant PROTOCOL_FEE_BPS = 500; // 5% + uint256 public constant PROTOCOL_FEE_BPS = 0; // 0% /// @dev The denominator used for calculating basis points. /// @notice 10,000 BPS = 100%. A fee of 500 BPS is therefore 5%. @@ -380,19 +380,17 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable { revert PayoutRecipientCannotBeZeroAddress(); } - uint256 protocolFee = (msg.value * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; - - /// @dev Remaining balance is the balance minus the protocol fee. - uint256 remainingBalance; - unchecked { - remainingBalance = msg.value - protocolFee; + if (PROTOCOL_FEE_BPS > 0) { + uint256 protocolFee = (msg.value * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; + uint256 remainingBalance; + unchecked { + remainingBalance = msg.value - protocolFee; + } + SafeTransferLib.safeTransferETH(PROTOCOL_FEE_RECIPIENT, protocolFee); + SafeTransferLib.safeTransferETH(_payoutRecipient, remainingBalance); + } else { + SafeTransferLib.safeTransferETH(_payoutRecipient, msg.value); } - - /// @dev Transfer the protocol fee to the protocol fee recipient. - SafeTransferLib.safeTransferETH(PROTOCOL_FEE_RECIPIENT, protocolFee); - - /// @dev Transfer the remaining balance to the payout recipient. - SafeTransferLib.safeTransferETH(_payoutRecipient, remainingBalance); } /*==============================================================