From bcdd5be1e0996ee54bd6d20fbff6a9a6a7b584dd Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 5 Dec 2024 17:12:35 -0500 Subject: [PATCH 01/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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 010c3c3cbbb62b5e2e3a80fbb55a47e9c06cebcf Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 12 Dec 2024 11:53:37 -0500 Subject: [PATCH 15/54] 1155 Signed-off-by: Adam Wolf --- contracts/common/IMagicDropMetadata.sol | 96 ++++++ .../ERC1155ConduitPreapprovedCloneable.sol | 41 +++ .../clones/ERC1155MagicDropCloneable.sol | 98 +++++++ .../ERC1155MagicDropMetadataCloneable.sol | 276 ++++++++++++++++++ .../interfaces/IERC1155MagicDropMetadata.sol | 42 +++ .../clones/ERC721MagicDropCloneable.sol | 19 -- .../ERC721MagicDropMetadataCloneable.sol | 10 +- .../erc721m/clones/TokenTransferValidator.sol | 33 +++ .../interfaces/IERC721MagicDropMetadata.sol | 104 +------ .../clones/ERC721MagicDropCloneable.t.sol | 12 +- 10 files changed, 599 insertions(+), 132 deletions(-) create mode 100644 contracts/common/IMagicDropMetadata.sol create mode 100644 contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol create mode 100644 contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol create mode 100644 contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol create mode 100644 contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol create mode 100644 contracts/nft/erc721m/clones/TokenTransferValidator.sol diff --git a/contracts/common/IMagicDropMetadata.sol b/contracts/common/IMagicDropMetadata.sol new file mode 100644 index 0000000..3c4825d --- /dev/null +++ b/contracts/common/IMagicDropMetadata.sol @@ -0,0 +1,96 @@ +// 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 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 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 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/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol new file mode 100644 index 0000000..ce573da --- /dev/null +++ b/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; + +/// @title ERC1155ConduitPreapprovedCloneable +/// @notice ERC1155 with the MagicEden conduit preapproved. +abstract contract ERC1155ConduitPreapprovedCloneable is ERC1155 { + /// @dev The canonical MagicEden conduit. + address internal constant _CONDUIT = 0x2052f8A2Ff46283B30084e5d84c89A2fdBE7f74b; + + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) + public + virtual + override + { + _safeTransfer(_by(), from, to, id, amount, data); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) public virtual override { + _safeBatchTransfer(_by(), from, to, ids, amounts, data); + } + + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + if (operator == _CONDUIT) return true; + return ERC1155.isApprovedForAll(owner, operator); + } + + function _by() internal view virtual returns (address result) { + assembly { + // `msg.sender == _CONDUIT ? address(0) : msg.sender`. + result := mul(iszero(eq(caller(), _CONDUIT)), caller()) + } + } +} diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol new file mode 100644 index 0000000..b6267fc --- /dev/null +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {ERC1155MagicDropMetadataCloneable} from "./ERC1155MagicDropMetadataCloneable.sol"; + +contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { + + mapping(uint256 => PublicStage) internal _publicStages; // tokenId => publicStage + + mapping(uint256 => AllowlistStage) internal _allowlistStages; // tokenId => allowlistStage + + address internal _payoutRecipient; + + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + + function mintPublic(uint256 tokenId, uint256 amount, address to) external {} + + function mintAllowlist(uint256 tokenId, uint256 amount, address to, bytes32[] calldata proof) external {} + + function burn(uint256 tokenId, uint256 amount, address from) external {} + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @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 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 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 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 + view + virtual + override(ERC1155ConduitPreapprovedCloneable) + returns (bool) + { + return interfaceId == type(IERC1155MagicDropMetadata).interfaceId || super.supportsInterface(interfaceId); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + function setupNewToken(SetupConfig calldata config) external onlyOwner {} + + function setPublicStage(uint256 tokenId, PublicStage calldata stage) external onlyOwner {} + + function setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) external onlyOwner {} + + function setPayoutRecipient(address newPayoutRecipient) external onlyOwner {} + + /*============================================================== + = INTERNAL HELPERS = + ==============================================================*/ + + function _splitProceeds() internal {} + + function _setPayoutRecipient(address newPayoutRecipient) internal {} + + function _setPublicStage(uint256 tokenId, PublicStage calldata stage) internal {} + + function _setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) internal {} + + /*============================================================== + = META = + ==============================================================*/ + + function contractNameAndVersion() public pure returns (string memory, string memory) { + return ("ERC1155MagicDropCloneable", "1.0.0"); + } + + /*============================================================== + = MISC = + ==============================================================*/ + + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } +} diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol new file mode 100644 index 0000000..7470433 --- /dev/null +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol @@ -0,0 +1,276 @@ +// 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 {Initializable} from "solady/src/utils/Initializable.sol"; + +import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; + +contract ERC1155MagicDropMetadataCloneable is + ERC1155ConduitPreapprovedCloneable, + IERC1155MagicDropMetadata, + ERC2981, + Ownable, + Initializable +{ + /// @dev The total supply of each token. + mapping(uint256 => TokenSupply) internal _tokenSupply; + + /// @dev The maximum number of tokens that can be minted by a single wallet. + mapping(uint256 => uint256) internal _walletLimit; + + /// @dev The total number of tokens minted by each user per token. + mapping(address => mapping(uint256 => uint256)) internal _totalMintedByUserPerToken; + + /// @dev The name of the collection. + string internal _name; + + /// @dev The symbol of the collection. + string internal _symbol; + + /// @dev The contract URI. + string internal _contractURI; + + /// @dev The base URI for the collection. + string internal _baseURI; + + /// @dev The provenance hash of the collection. + bytes32 internal _provenanceHash; + + /// @dev The address that receives royalty payments. + address internal _royaltyReceiver; + + /// @dev The royalty basis points. + uint256 internal _royaltyBps; + + event MagicDropTokenDeployed(); + + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + /// @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-1155 name of the collection. + /// @param _symbol The ERC-1155 symbol of the collection. + /// @param _owner The address designated as the initial owner of the contract. + function __ERC1155MagicDropMetadataCloneable__init(string memory name_, string memory symbol_, address owner_) + internal + onlyInitializing + { + _name = name_; + _symbol = symbol_; + _initializeOwner(owner_); + } + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @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 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 The maximum number of tokens that can ever be minted by this contract. + /// @param tokenId The ID of the token. + /// @return The maximum supply of tokens. + function maxSupply(uint256 tokenId) public view returns (uint256) { + return _tokenSupply[tokenId].maxSupply; + } + + /// @notice Return the total supply of a token. + /// @param tokenId The ID of the token. + /// @return The total supply of token. + function totalSupply(uint256 tokenId) public view returns (uint256) { + return _tokenSupply[tokenId].totalSupply; + } + + /// @notice Return the total number of tokens minted for a specific token. + /// @param tokenId The ID of the token. + /// @return The total number of tokens minted. + function totalMinted(uint256 tokenId) public view returns (uint256) { + return _tokenSupply[tokenId].totalMinted; + } + + /// @notice Return the maximum number of tokens any single wallet can mint for a specific token. + /// @param tokenId The ID of the token. + /// @return The minting limit per wallet. + function walletLimit(uint256 tokenId) public view returns (uint256) { + return _walletLimit[tokenId]; + } + + /// @notice The assigned provenance hash used to ensure the integrity of the metadata ordering. + /// @return The provenance hash of the token. + function provenanceHash(uint256 tokenId) public view returns (bytes32) { + return _provenanceHash[tokenId]; + } + + /// @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 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 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 + virtual + override(ERC1155ConduitPreapprovedCloneable, IERC1155ConduitPreapproved, ERC2981) + returns (bool) + { + return interfaceId == 0x2a55205a // ERC-2981 royalties + || interfaceId == 0x49064906 // ERC-4906 metadata updates + || super.supportsInterface(interfaceId); + } + + /// @notice Returns the URI for a given token ID. + /// @dev This returns the base URI for all tokens. + /// @param tokenId The ID of the token. + /// @return The URI for the token. + function uri(uint256 /* tokenId */) public view returns (string memory) { + return _baseURI; + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @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 tokenId The ID of the token to update. + /// @param newMaxSupply The new maximum supply. + function setMaxSupply(uint256 tokenId, uint256 newMaxSupply) external onlyOwner { + _setMaxSupply(tokenId, newMaxSupply); + } + + + /// @notice Updates the per-wallet minting limit. + /// @dev This can be changed at any time to adjust distribution constraints. + /// @param tokenId The ID of the token. + /// @param newWalletLimit The new per-wallet limit on minted tokens. + function setWalletLimit(uint256 tokenId, uint256 newWalletLimit) external onlyOwner { + _setWalletLimit(tokenId, 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 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 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 Internal function setting the maximum token supply. + /// @dev Cannot increase beyond the original max supply. Cannot set below the current minted amount. + /// @param tokenId The ID of the token. + /// @param newMaxSupply The new maximum supply. + function _setMaxSupply(uint256 tokenId, uint256 newMaxSupply) internal { + if (_maxSupply != 0 && newMaxSupply > _maxSupply) { + revert MaxSupplyCannotBeIncreased(); + } + + if (newMaxSupply < _tokenSupply[tokenId].maxSupply) { + revert MaxSupplyCannotBeLessThanCurrentSupply(); + } + + if (newMaxSupply > 2 ** 64 - 1) { + revert MaxSupplyCannotBeGreaterThan2ToThe64thPower(); + } + + _tokenSupply[tokenId].maxSupply = uint64(newMaxSupply); + + emit MaxSupplyUpdated(tokenId, newMaxSupply); + } + + /// @notice Internal function setting the per-wallet minting limit. + /// @param tokenId The ID of the token. + /// @param newWalletLimit The new per-wallet limit on minted tokens. + function _setWalletLimit(uint256 tokenId, uint256 newWalletLimit) internal { + _walletLimit[tokenId] = newWalletLimit; + emit WalletLimitUpdated(tokenId, newWalletLimit); + } + + /// @notice Internal function setting the provenance hash. + /// @param tokenId The ID of the token. + /// @param newProvenanceHash The new provenance hash. + function _setProvenanceHash(uint256 tokenId, bytes32 newProvenanceHash) internal { + if (_tokenSupply[tokenId].totalMinted > 0) { + revert ProvenanceHashCannotBeUpdated(); + } + + bytes32 oldProvenanceHash = _provenanceHash[tokenId]; + _provenanceHash[tokenId] = newProvenanceHash; + emit ProvenanceHashUpdated(tokenId, oldProvenanceHash, newProvenanceHash); + } +} diff --git a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol new file mode 100644 index 0000000..2982246 --- /dev/null +++ b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; + +interface IERC1155MagicDropMetadata is IMagicDropMetadata { + struct TokenSupply { + uint64 maxSupply; + uint64 totalSupply; + uint64 totalMinted; + } + + error MaxSupplyCannotBeGreaterThan2ToThe64thPower(); + + event MaxSupplyUpdated(uint256 tokenId, uint256 oldMaxSupply, uint256 newMaxSupply); + + event ProvenanceHashUpdated(uint256 tokenId, bytes32 oldHash, bytes32 newHash); + + error ProvenanceHashCannotBeUpdated(uint256 tokenId); + + error WalletLimitExceeded(uint256 tokenId); + + function setMaxSupply(uint256 tokenId, uint256 newMaxSupply) external; + + function setWalletLimit(uint256 tokenId, uint256 walletLimit) external; + + function setProvenanceHash(uint256 tokenId, bytes32 provenanceHash) external; + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function maxSupply(uint256 tokenId) external view returns (uint256); + + function totalSupply(uint256 tokenId) external view returns (uint256); + + function totalMinted(uint256 tokenId) external view returns (uint256); + + function provenanceHash(uint256 tokenId) external view returns (bytes32); + + function walletLimit(uint256 tokenId) external view returns (uint256); +} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index aace594..ed20958 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(); } /*============================================================== diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index aca7b46..a15ab00 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -30,6 +30,8 @@ contract ERC721MagicDropMetadataCloneable is /// @param owner The address of the contract owner. function __ERC721MagicDropMetadataCloneable__init(address owner) internal onlyInitializing { _initializeOwner(owner); + + emit MagicDropTokenDeployed(); } /*============================================================== @@ -70,7 +72,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. @@ -184,12 +186,6 @@ contract ERC721MagicDropMetadataCloneable is = 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 { diff --git a/contracts/nft/erc721m/clones/TokenTransferValidator.sol b/contracts/nft/erc721m/clones/TokenTransferValidator.sol new file mode 100644 index 0000000..977cd71 --- /dev/null +++ b/contracts/nft/erc721m/clones/TokenTransferValidator.sol @@ -0,0 +1,33 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.17; + +// import { ICreatorToken } from "../interfaces/ICreatorToken.sol"; + +// /** +// * @title TokenTransferValidator +// * @notice Functionality to use a transfer validator. +// */ +// abstract contract TokenTransferValidator is ICreatorToken { +// /// @dev Store the transfer validator. The null address means no transfer validator is set. +// address internal _transferValidator; + +// /// @notice Revert with an error if the transfer validator is being set to the same address. +// error SameTransferValidator(); + +// /// @notice Returns the currently active transfer validator. +// /// The null address means no transfer validator is set. +// function getTransferValidator() external view returns (address) { find similar +// return _transferValidator; +// } + +// /// @notice Set the transfer validator. +// /// The external method that uses this must include access control. +// function _setTransferValidator(address newValidator) internal { +// address oldValidator = _transferValidator; +// if (oldValidator == newValidator) { +// revert SameTransferValidator(); +// } +// _transferValidator = newValidator; +// emit TransferValidatorUpdated(oldValidator, newValidator); +// } +// } diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol index 1286a32..f805669 100644 --- a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol +++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol @@ -1,19 +1,9 @@ // 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/IMagicDropMetadata.sol"; +interface IERC721MagicDropMetadata is IMagicDropMetadata { /// @notice Emitted when the wallet limit is updated. /// @param _walletLimit The new wallet limit. event WalletLimitUpdated(uint256 _walletLimit); @@ -23,92 +13,21 @@ interface IERC721MagicDropMetadata { /// @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 Thrown when a mint would exceed the wallet-specific minting limit. + error WalletLimitExceeded(); /// @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 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 Updates the per-wallet token holding limit /// @dev Used to prevent token concentration and ensure fair distribution @@ -121,11 +40,4 @@ interface IERC721MagicDropMetadata { /// 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/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 0ca0229..6d028ab 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -84,11 +84,7 @@ contract ERC721MagicDropCloneableTest is Test { price: 0.005 ether, merkleRoot: merkleHelper.getRoot() }), - publicStage: PublicStage({ - startTime: uint64(publicStart), - endTime: uint64(publicEnd), - price: 0.01 ether - }), + publicStage: PublicStage({startTime: uint64(publicStart), endTime: uint64(publicEnd), price: 0.01 ether}), payoutRecipient: payoutRecipient, provenanceHash: keccak256("some-provenance") }); @@ -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); From 5d373e75f146bfbda1bb182f36b17a5ea79444c5 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 12 Dec 2024 12:54:04 -0500 Subject: [PATCH 16/54] fix build Signed-off-by: Adam Wolf --- contracts/common/IMagicDropMetadata.sol | 11 --- .../clones/ERC1155MagicDropCloneable.sol | 16 +-- .../ERC1155MagicDropMetadataCloneable.sol | 97 +++++++------------ contracts/nft/erc1155m/clones/Types.sol | 43 ++++++++ .../interfaces/IERC1155MagicDropMetadata.sol | 12 +-- .../clones/ERC721MagicDropCloneable.sol | 6 +- .../ERC721MagicDropMetadataCloneable.sol | 35 +------ .../erc721m/clones/TokenTransferValidator.sol | 33 ------- contracts/nft/erc721m/clones/Types.sol | 4 - .../interfaces/IERC721MagicDropMetadata.sol | 30 +++--- .../clones/ERC721MagicDropCloneable.t.sol | 8 +- .../ERC721MagicDropMetadataCloneable.t.sol | 57 +++-------- 12 files changed, 122 insertions(+), 230 deletions(-) create mode 100644 contracts/nft/erc1155m/clones/Types.sol delete mode 100644 contracts/nft/erc721m/clones/TokenTransferValidator.sol diff --git a/contracts/common/IMagicDropMetadata.sol b/contracts/common/IMagicDropMetadata.sol index 3c4825d..cddc0eb 100644 --- a/contracts/common/IMagicDropMetadata.sol +++ b/contracts/common/IMagicDropMetadata.sol @@ -50,11 +50,6 @@ interface IMagicDropMetadata { /// @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 address that receives royalty payments /// @dev Used in conjunction with royaltyBps for EIP-2981 royalty standard /// @return The address designated to receive royalty payments @@ -81,12 +76,6 @@ interface IMagicDropMetadata { /// @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 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%) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index b6267fc..6232bb5 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -2,9 +2,13 @@ pragma solidity ^0.8.22; import {ERC1155MagicDropMetadataCloneable} from "./ERC1155MagicDropMetadataCloneable.sol"; +import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedCloneable.sol"; +import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; +import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; -contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, ReentrancyGuard { mapping(uint256 => PublicStage) internal _publicStages; // tokenId => publicStage mapping(uint256 => AllowlistStage) internal _allowlistStages; // tokenId => allowlistStage @@ -33,14 +37,14 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @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; + function getPublicStage(uint256 tokenId) external view returns (PublicStage memory) { + return _publicStages[tokenId]; } /// @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; + function getAllowlistStage(uint256 tokenId) external view returns (AllowlistStage memory) { + return _allowlistStages[tokenId]; } /// @notice Indicates whether the contract implements a given interface. @@ -50,7 +54,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { public view virtual - override(ERC1155ConduitPreapprovedCloneable) + override(ERC1155MagicDropMetadataCloneable) returns (bool) { return interfaceId == type(IERC1155MagicDropMetadata).interfaceId || super.supportsInterface(interfaceId); diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol index 7470433..cca6860 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol @@ -5,7 +5,9 @@ import {ERC2981} from "solady/src/tokens/ERC2981.sol"; import {Ownable} from "solady/src/auth/Ownable.sol"; import {Initializable} from "solady/src/utils/Initializable.sol"; +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; +import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedCloneable.sol"; contract ERC1155MagicDropMetadataCloneable is ERC1155ConduitPreapprovedCloneable, @@ -35,26 +37,21 @@ contract ERC1155MagicDropMetadataCloneable is /// @dev The base URI for the collection. string internal _baseURI; - /// @dev The provenance hash of the collection. - bytes32 internal _provenanceHash; - /// @dev The address that receives royalty payments. address internal _royaltyReceiver; /// @dev The royalty basis points. uint256 internal _royaltyBps; - event MagicDropTokenDeployed(); - /*============================================================== = INITIALIZERS = ==============================================================*/ /// @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-1155 name of the collection. - /// @param _symbol The ERC-1155 symbol of the collection. - /// @param _owner The address designated as the initial owner of the contract. + /// @param name_ The ERC-1155 name of the collection. + /// @param symbol_ The ERC-1155 symbol of the collection. + /// @param owner_ The address designated as the initial owner of the contract. function __ERC1155MagicDropMetadataCloneable__init(string memory name_, string memory symbol_, address owner_) internal onlyInitializing @@ -62,20 +59,30 @@ contract ERC1155MagicDropMetadataCloneable is _name = name_; _symbol = symbol_; _initializeOwner(owner_); + + emit MagicDropTokenDeployed(); } /*============================================================== = PUBLIC VIEW METHODS = ==============================================================*/ + /// @notice Returns the name of the collection. + function name() public view returns (string memory) { + return _name; + } + + /// @notice Returns the symbol of the collection. + function symbol() public view returns (string memory) { + return _symbol; + } + /// @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 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; } @@ -108,12 +115,6 @@ contract ERC1155MagicDropMetadataCloneable is return _walletLimit[tokenId]; } - /// @notice The assigned provenance hash used to ensure the integrity of the metadata ordering. - /// @return The provenance hash of the token. - function provenanceHash(uint256 tokenId) public view returns (bytes32) { - return _provenanceHash[tokenId]; - } - /// @notice The address designated to receive royalty payments on secondary sales. /// @return The royalty receiver address. function royaltyAddress() public view returns (address) { @@ -130,13 +131,7 @@ contract ERC1155MagicDropMetadataCloneable is /// @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 - virtual - override(ERC1155ConduitPreapprovedCloneable, IERC1155ConduitPreapproved, ERC2981) - returns (bool) - { + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, ERC2981) returns (bool) { return interfaceId == 0x2a55205a // ERC-2981 royalties || interfaceId == 0x49064906 // ERC-4906 metadata updates || super.supportsInterface(interfaceId); @@ -144,9 +139,8 @@ contract ERC1155MagicDropMetadataCloneable is /// @notice Returns the URI for a given token ID. /// @dev This returns the base URI for all tokens. - /// @param tokenId The ID of the token. /// @return The URI for the token. - function uri(uint256 /* tokenId */) public view returns (string memory) { + function uri(uint256 /* tokenId */ ) public view override returns (string memory) { return _baseURI; } @@ -176,7 +170,6 @@ contract ERC1155MagicDropMetadataCloneable is _setMaxSupply(tokenId, newMaxSupply); } - /// @notice Updates the per-wallet minting limit. /// @dev This can be changed at any time to adjust distribution constraints. /// @param tokenId The ID of the token. @@ -185,21 +178,6 @@ contract ERC1155MagicDropMetadataCloneable is _setWalletLimit(tokenId, 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 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. @@ -223,12 +201,19 @@ contract ERC1155MagicDropMetadataCloneable is /// @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; + _baseURI = newBaseURI; - if (totalSupply() != 0) { - // Notify EIP-4906 compliant observers of a metadata update. - emit BatchMetadataUpdate(0, totalSupply() - 1); - } + // Notify EIP-4906 compliant observers of a metadata update. + emit BatchMetadataUpdate(0, type(uint256).max); + } + + function _setContractURI(string calldata newContractURI) internal { + _contractURI = newContractURI; + } + + function _setRoyaltyInfo(address newReceiver, uint96 newBps) internal { + _royaltyReceiver = newReceiver; + _royaltyBps = newBps; } /// @notice Internal function setting the maximum token supply. @@ -236,11 +221,12 @@ contract ERC1155MagicDropMetadataCloneable is /// @param tokenId The ID of the token. /// @param newMaxSupply The new maximum supply. function _setMaxSupply(uint256 tokenId, uint256 newMaxSupply) internal { - if (_maxSupply != 0 && newMaxSupply > _maxSupply) { + uint256 oldMaxSupply = _tokenSupply[tokenId].maxSupply; + if (oldMaxSupply != 0 && newMaxSupply > oldMaxSupply) { revert MaxSupplyCannotBeIncreased(); } - if (newMaxSupply < _tokenSupply[tokenId].maxSupply) { + if (newMaxSupply < oldMaxSupply) { revert MaxSupplyCannotBeLessThanCurrentSupply(); } @@ -250,7 +236,7 @@ contract ERC1155MagicDropMetadataCloneable is _tokenSupply[tokenId].maxSupply = uint64(newMaxSupply); - emit MaxSupplyUpdated(tokenId, newMaxSupply); + emit MaxSupplyUpdated(tokenId, oldMaxSupply, newMaxSupply); } /// @notice Internal function setting the per-wallet minting limit. @@ -260,17 +246,4 @@ contract ERC1155MagicDropMetadataCloneable is _walletLimit[tokenId] = newWalletLimit; emit WalletLimitUpdated(tokenId, newWalletLimit); } - - /// @notice Internal function setting the provenance hash. - /// @param tokenId The ID of the token. - /// @param newProvenanceHash The new provenance hash. - function _setProvenanceHash(uint256 tokenId, bytes32 newProvenanceHash) internal { - if (_tokenSupply[tokenId].totalMinted > 0) { - revert ProvenanceHashCannotBeUpdated(); - } - - bytes32 oldProvenanceHash = _provenanceHash[tokenId]; - _provenanceHash[tokenId] = newProvenanceHash; - emit ProvenanceHashUpdated(tokenId, oldProvenanceHash, newProvenanceHash); - } } diff --git a/contracts/nft/erc1155m/clones/Types.sol b/contracts/nft/erc1155m/clones/Types.sol new file mode 100644 index 0000000..c1a7a97 --- /dev/null +++ b/contracts/nft/erc1155m/clones/Types.sol @@ -0,0 +1,43 @@ +// 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; +} diff --git a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol index 2982246..694e024 100644 --- a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol +++ b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol @@ -14,17 +14,13 @@ interface IERC1155MagicDropMetadata is IMagicDropMetadata { event MaxSupplyUpdated(uint256 tokenId, uint256 oldMaxSupply, uint256 newMaxSupply); - event ProvenanceHashUpdated(uint256 tokenId, bytes32 oldHash, bytes32 newHash); - - error ProvenanceHashCannotBeUpdated(uint256 tokenId); - error WalletLimitExceeded(uint256 tokenId); - function setMaxSupply(uint256 tokenId, uint256 newMaxSupply) external; + event WalletLimitUpdated(uint256 tokenId, uint256 newWalletLimit); - function setWalletLimit(uint256 tokenId, uint256 walletLimit) external; + function setMaxSupply(uint256 tokenId, uint256 newMaxSupply) external; - function setProvenanceHash(uint256 tokenId, bytes32 provenanceHash) external; + function setWalletLimit(uint256 tokenId, uint256 newWalletLimit) external; function name() external view returns (string memory); @@ -36,7 +32,5 @@ interface IERC1155MagicDropMetadata is IMagicDropMetadata { function totalMinted(uint256 tokenId) external view returns (uint256); - function provenanceHash(uint256 tokenId) external view returns (bytes32); - function walletLimit(uint256 tokenId) external view returns (uint256); } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index ed20958..84ad8c5 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -198,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) { @@ -228,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. diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index a15ab00..1705d79 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, @@ -54,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; @@ -93,12 +87,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) { @@ -159,13 +147,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. @@ -220,18 +201,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%). diff --git a/contracts/nft/erc721m/clones/TokenTransferValidator.sol b/contracts/nft/erc721m/clones/TokenTransferValidator.sol deleted file mode 100644 index 977cd71..0000000 --- a/contracts/nft/erc721m/clones/TokenTransferValidator.sol +++ /dev/null @@ -1,33 +0,0 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.17; - -// import { ICreatorToken } from "../interfaces/ICreatorToken.sol"; - -// /** -// * @title TokenTransferValidator -// * @notice Functionality to use a transfer validator. -// */ -// abstract contract TokenTransferValidator is ICreatorToken { -// /// @dev Store the transfer validator. The null address means no transfer validator is set. -// address internal _transferValidator; - -// /// @notice Revert with an error if the transfer validator is being set to the same address. -// error SameTransferValidator(); - -// /// @notice Returns the currently active transfer validator. -// /// The null address means no transfer validator is set. -// function getTransferValidator() external view returns (address) { find similar -// return _transferValidator; -// } - -// /// @notice Set the transfer validator. -// /// The external method that uses this must include access control. -// function _setTransferValidator(address newValidator) internal { -// address oldValidator = _transferValidator; -// if (oldValidator == newValidator) { -// revert SameTransferValidator(); -// } -// _transferValidator = newValidator; -// emit TransferValidatorUpdated(oldValidator, newValidator); -// } -// } 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 f805669..4253587 100644 --- a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol +++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol @@ -8,36 +8,32 @@ interface IERC721MagicDropMetadata is IMagicDropMetadata { /// @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 Throw when the provenance hash cannot be updated. - error ProvenanceHashCannotBeUpdated(); + /// @notice Emitted when the max supply is updated. + /// @param newMaxSupply The new max supply. + event MaxSupplyUpdated(uint256 newMaxSupply); /// @notice Thrown when a mint would exceed the wallet-specific minting limit. error WalletLimitExceeded(); - /// @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 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 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 /// 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 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; } diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 6d028ab..c1d3b7c 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"; @@ -85,8 +86,7 @@ contract ERC721MagicDropCloneableTest is Test { merkleRoot: merkleHelper.getRoot() }), publicStage: PublicStage({startTime: uint64(publicStart), endTime: uint64(publicEnd), price: 0.01 ether}), - payoutRecipient: payoutRecipient, - provenanceHash: keccak256("some-provenance") + payoutRecipient: payoutRecipient }); vm.prank(owner); @@ -165,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(); } @@ -237,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(); } diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol index 6cbee54..49cdca2 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/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(""); From e98e6e491c8bcc7811782309efb3e7babd4964f7 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 12 Dec 2024 12:55:48 -0500 Subject: [PATCH 17/54] remove test Signed-off-by: Adam Wolf --- test/erc721m/clones/ERC721MagicDropCloneable.t.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index c1d3b7c..4359961 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -431,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); From d6387cf78436719466a25f884c95bdb2523f46fc Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 12 Dec 2024 14:10:41 -0500 Subject: [PATCH 18/54] 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 2d8a8db38d00a370d193ea3d127b30fb9a006bf8 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 12 Dec 2024 21:26:32 -0500 Subject: [PATCH 19/54] mint funcs Signed-off-by: Adam Wolf --- .../common/interfaces/IMagicDropMetadata.sol | 2 +- .../clones/ERC1155MagicDropCloneable.sol | 146 +++++++++++++++++- .../ERC1155MagicDropMetadataCloneable.sol | 44 +++--- contracts/nft/erc1155m/clones/Types.sol | 2 + .../clones/ERC721MagicDropCloneable.sol | 2 +- .../ERC721MagicDropMetadataCloneable.sol | 5 +- .../clones/ERC721MagicDropCloneable.t.sol | 2 +- .../ERC721MagicDropMetadataCloneable.t.sol | 2 +- 8 files changed, 172 insertions(+), 33 deletions(-) diff --git a/contracts/common/interfaces/IMagicDropMetadata.sol b/contracts/common/interfaces/IMagicDropMetadata.sol index 2f2330a..cddc0eb 100644 --- a/contracts/common/interfaces/IMagicDropMetadata.sol +++ b/contracts/common/interfaces/IMagicDropMetadata.sol @@ -82,4 +82,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/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 6232bb5..adb5b1e 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; +import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; + import {ERC1155MagicDropMetadataCloneable} from "./ERC1155MagicDropMetadataCloneable.sol"; import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedCloneable.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; @@ -15,15 +17,86 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra address internal _payoutRecipient; + error InvalidStageTime(); + + error PublicStageNotActive(); + + error AllowlistStageNotActive(); + + error InvalidPublicStageTime(); + + error InvalidAllowlistStageTime(); + /*============================================================== = PUBLIC WRITE METHODS = ==============================================================*/ - function mintPublic(uint256 tokenId, uint256 amount, address to) external {} + function mintPublic(uint256 tokenId, uint256 qty, address to, bytes memory data) external { + PublicStage memory stage = _publicStages[tokenId]; + if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { + revert PublicStageNotActive(); + } + + uint256 requiredPayment = stage.price * qty; + if (msg.value < requiredPayment) { + revert NotEnoughValue(); + } + + if (_totalMintedByUserPerToken[to][tokenId] + qty > this.walletLimit(tokenId)) { + revert WalletLimitExceeded(); + } + + if (stage.price != 0) { + _splitProceeds(); + } + + _mint(to, tokenId, qty, data); + } + + function mintAllowlist(uint256 tokenId, uint256 qty, address to, bytes32[] calldata proof, bytes memory data) external { + AllowlistStage memory stage = _allowlistStages[tokenId]; + if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { + revert AllowlistStageNotActive(); + } + + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to)))) { + revert InvalidProof(); + } + + uint256 requiredPayment = stage.price * qty; + if (msg.value < requiredPayment) { + revert NotEnoughValue(); + } + + if (stage.price != 0) { + _splitProceeds(); + } - function mintAllowlist(uint256 tokenId, uint256 amount, address to, bytes32[] calldata proof) external {} + _increaseSupplyOnMint(to, tokenId, qty); + _mint(to, tokenId, qty, data); + } + + function burn(uint256 tokenId, uint256 qty, address from) external { + _reduceSupplyOnBurn(tokenId, qty); + _burn(from, tokenId, qty); + } + + function burn(address by, address from, uint256 id, uint256 qty) external { + _reduceSupplyOnBurn(id, qty); + _burn(by, from, id, qty); + } - function burn(uint256 tokenId, uint256 amount, address from) external {} + function burnBatch(address by, address from, uint256[] calldata ids, uint256[] calldata qty) external { + uint256 length = ids.length; + for (uint256 i = 0; i < length; i++) { + _reduceSupplyOnBurn(ids[i], qty[i]); + unchecked { + ++i; + } + } + + _burnBatch(by, from, ids, qty); + } /*============================================================== = PUBLIC VIEW METHODS = @@ -64,9 +137,50 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra = ADMIN OPERATIONS = ==============================================================*/ - function setupNewToken(SetupConfig calldata config) external onlyOwner {} + function setup(SetupConfig calldata config) external onlyOwner { + if (config.maxSupply > 0) { + _setMaxSupply(config.tokenId, config.maxSupply); + } + + if (config.walletLimit > 0) { + _setWalletLimit(config.tokenId, config.walletLimit); + } + + if (bytes(config.baseURI).length > 0) { + _setBaseURI(config.baseURI); + } + + if (bytes(config.contractURI).length > 0) { + _setContractURI(config.contractURI); + } + + if (config.allowlistStage.startTime != 0 || config.allowlistStage.endTime != 0) { + _setAllowlistStage(config.tokenId, config.allowlistStage); + } + + if (config.publicStage.startTime != 0 || config.publicStage.endTime != 0) { + _setPublicStage(config.tokenId, config.publicStage); + } + + if (config.payoutRecipient != address(0)) { + _setPayoutRecipient(config.payoutRecipient); + } + } + + function setPublicStage(PublicStage calldata stage) external onlyOwner { + if (stage.startTime >= stage.endTime) { + revert InvalidStageTime(); + } - function setPublicStage(uint256 tokenId, PublicStage calldata stage) external onlyOwner {} + // Ensure the public stage starts after the allowlist stage ends + if (_allowlistStages[stage.tokenId].startTime != 0 && _allowlistStages[stage.tokenId].endTime != 0) { + if (stage.startTime < _allowlistStages[stage.tokenId].endTime) { + revert InvalidPublicStageTime(); + } + } + + _publicStages[stage.tokenId] = stage; + } function setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) external onlyOwner {} @@ -84,6 +198,28 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra function _setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) internal {} + function _reduceSupplyOnBurn(uint256 tokenId, uint256 qty) internal { + TokenSupply storage supply = _tokenSupply[tokenId]; + unchecked { + supply.totalSupply -= qty; + } + } + + function _increaseSupplyOnMint(address to, uint256 tokenId, uint256 qty) internal { + TokenSupply storage supply = _tokenSupply[tokenId]; + + if (supply.totalMinted + qty > supply.maxSupply) { + revert CannotExceedMaxSupply(); + } + + unchecked { + supply.totalSupply += uint64(qty); + supply.totalMinted += uint64(qty); + + _totalMintedByUserPerToken[to][tokenId] += uint64(qty); + } + } + /*============================================================== = META = ==============================================================*/ diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol index cca6860..7bcb752 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol @@ -16,15 +16,6 @@ contract ERC1155MagicDropMetadataCloneable is Ownable, Initializable { - /// @dev The total supply of each token. - mapping(uint256 => TokenSupply) internal _tokenSupply; - - /// @dev The maximum number of tokens that can be minted by a single wallet. - mapping(uint256 => uint256) internal _walletLimit; - - /// @dev The total number of tokens minted by each user per token. - mapping(address => mapping(uint256 => uint256)) internal _totalMintedByUserPerToken; - /// @dev The name of the collection. string internal _name; @@ -43,6 +34,15 @@ contract ERC1155MagicDropMetadataCloneable is /// @dev The royalty basis points. uint256 internal _royaltyBps; + /// @dev The total supply of each token. + mapping(uint256 => TokenSupply) internal _tokenSupply; + + /// @dev The maximum number of tokens that can be minted by a single wallet. + mapping(uint256 => uint256) internal _walletLimit; + + /// @dev The total number of tokens minted by each user per token. + mapping(address => mapping(uint256 => uint256)) internal _totalMintedByUserPerToken; + /*============================================================== = INITIALIZERS = ==============================================================*/ @@ -87,6 +87,18 @@ contract ERC1155MagicDropMetadataCloneable is return _contractURI; } + /// @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 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 The maximum number of tokens that can ever be minted by this contract. /// @param tokenId The ID of the token. /// @return The maximum supply of tokens. @@ -115,18 +127,6 @@ contract ERC1155MagicDropMetadataCloneable is return _walletLimit[tokenId]; } - /// @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 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 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. @@ -134,7 +134,7 @@ contract ERC1155MagicDropMetadataCloneable is function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, ERC2981) returns (bool) { return interfaceId == 0x2a55205a // ERC-2981 royalties || interfaceId == 0x49064906 // ERC-4906 metadata updates - || super.supportsInterface(interfaceId); + || interfaceId == type(IERC1155MagicDropMetadata).interfaceId || ERC1155.supportsInterface(interfaceId); } /// @notice Returns the URI for a given token ID. diff --git a/contracts/nft/erc1155m/clones/Types.sol b/contracts/nft/erc1155m/clones/Types.sol index c1a7a97..25fd5c6 100644 --- a/contracts/nft/erc1155m/clones/Types.sol +++ b/contracts/nft/erc1155m/clones/Types.sol @@ -23,6 +23,8 @@ struct AllowlistStage { } struct SetupConfig { + /// @dev The token ID of the token. + uint256 tokenId; /// @dev The maximum number of tokens that can be minted. /// - Can be decreased if current supply < new max supply /// - Cannot be increased once set diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 5b93832..84ad8c5 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 e23cb6b..e38f7e5 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -76,6 +76,7 @@ contract ERC721MagicDropMetadataCloneable is } /// @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; } @@ -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 { @@ -222,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 c1c1ff2..4359961 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 62c20c9e0219c75a3bef09afb675bbe8e0ce549e Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 16 Dec 2024 13:46:25 -0500 Subject: [PATCH 20/54] fix Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 32 ++++++++++++------- .../clones/ERC721MagicDropCloneable.sol | 2 +- .../clones/ERC721MagicDropCloneable.t.sol | 4 +-- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index adb5b1e..b8a8d1d 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -11,11 +11,13 @@ import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, ReentrancyGuard { + address internal _payoutRecipient; + mapping(uint256 => PublicStage) internal _publicStages; // tokenId => publicStage mapping(uint256 => AllowlistStage) internal _allowlistStages; // tokenId => allowlistStage - address internal _payoutRecipient; + error InvalidProof(); error InvalidStageTime(); @@ -27,11 +29,13 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra error InvalidAllowlistStageTime(); + error NotEnoughValue(); + /*============================================================== = PUBLIC WRITE METHODS = ==============================================================*/ - function mintPublic(uint256 tokenId, uint256 qty, address to, bytes memory data) external { + function mintPublic(uint256 tokenId, uint256 qty, address to, bytes memory data) external payable nonReentrant { PublicStage memory stage = _publicStages[tokenId]; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); @@ -43,7 +47,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra } if (_totalMintedByUserPerToken[to][tokenId] + qty > this.walletLimit(tokenId)) { - revert WalletLimitExceeded(); + revert WalletLimitExceeded(tokenId); } if (stage.price != 0) { @@ -53,7 +57,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra _mint(to, tokenId, qty, data); } - function mintAllowlist(uint256 tokenId, uint256 qty, address to, bytes32[] calldata proof, bytes memory data) external { + function mintAllowlist(uint256 tokenId, uint256 qty, address to, bytes32[] calldata proof, bytes memory data) external payable nonReentrant { AllowlistStage memory stage = _allowlistStages[tokenId]; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert AllowlistStageNotActive(); @@ -95,7 +99,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra } } - _burnBatch(by, from, ids, qty); + _batchBurn(by, from, ids, qty); } /*============================================================== @@ -167,24 +171,28 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra } } - function setPublicStage(PublicStage calldata stage) external onlyOwner { + function setPublicStage(uint256 tokenId, PublicStage calldata stage) external onlyOwner { if (stage.startTime >= stage.endTime) { revert InvalidStageTime(); } // Ensure the public stage starts after the allowlist stage ends - if (_allowlistStages[stage.tokenId].startTime != 0 && _allowlistStages[stage.tokenId].endTime != 0) { - if (stage.startTime < _allowlistStages[stage.tokenId].endTime) { + if (_allowlistStages[tokenId].startTime != 0 && _allowlistStages[tokenId].endTime != 0) { + if (stage.startTime < _allowlistStages[tokenId].endTime) { revert InvalidPublicStageTime(); } } - _publicStages[stage.tokenId] = stage; + _publicStages[tokenId] = stage; } - function setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) external onlyOwner {} + function setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) external onlyOwner { + _allowlistStages[tokenId] = stage; + } - function setPayoutRecipient(address newPayoutRecipient) external onlyOwner {} + function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { + _setPayoutRecipient(newPayoutRecipient); + } /*============================================================== = INTERNAL HELPERS = @@ -201,7 +209,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra function _reduceSupplyOnBurn(uint256 tokenId, uint256 qty) internal { TokenSupply storage supply = _tokenSupply[tokenId]; unchecked { - supply.totalSupply -= qty; + supply.totalSupply -= uint64(qty); } } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 84ad8c5..3f40af8 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -97,7 +97,7 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc /// 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 { + function mintPublic(uint256 qty, address to) external payable nonReentrant { PublicStage memory stage = _publicStage; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 4359961..daf1a2e 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -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)); } From 99c0a83f6caaf8e8b36d8e896acc9857aabdbd50 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 10:42:48 -0500 Subject: [PATCH 21/54] comments Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index b8a8d1d..86f3277 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -11,25 +11,41 @@ import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, ReentrancyGuard { + /// @dev Address that receives the primary sale proceeds of minted tokens. + /// Configurable by the owner. If unset, withdrawals may fail. address internal _payoutRecipient; + /// @dev Configuration of the public mint stage, including timing and price. + /// @notice Public mints occur only if the current timestamp is within [startTime, endTime]. mapping(uint256 => PublicStage) internal _publicStages; // tokenId => publicStage + /// @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. mapping(uint256 => AllowlistStage) internal _allowlistStages; // tokenId => allowlistStage - error InvalidProof(); - - error InvalidStageTime(); - + /// @notice Thrown when attempting to mint during a public stage that is not currently active. error PublicStageNotActive(); + /// @notice Thrown when attempting to mint during an allowlist stage that is not currently active. error AllowlistStageNotActive(); + /// @notice Thrown when the provided ETH value for a mint is insufficient. + error NotEnoughValue(); + + /// @notice Thrown when the provided Merkle proof for an allowlist mint is invalid. + error InvalidProof(); + + /// @notice Thrown when a stage's start or end time configuration is invalid. + error InvalidStageTime(); + + /// @notice Thrown when the public stage timing conflicts with the allowlist stage timing. error InvalidPublicStageTime(); + /// @notice Thrown when the allowlist stage timing conflicts with the public stage timing. error InvalidAllowlistStageTime(); - error NotEnoughValue(); + /// @notice Thrown when the payout recipient is set to a zero address. + error PayoutRecipientCannotBeZeroAddress(); /*============================================================== = PUBLIC WRITE METHODS = @@ -200,7 +216,9 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra function _splitProceeds() internal {} - function _setPayoutRecipient(address newPayoutRecipient) internal {} + function _setPayoutRecipient(address newPayoutRecipient) internal { + _payoutRecipient = newPayoutRecipient; + } function _setPublicStage(uint256 tokenId, PublicStage calldata stage) internal {} From 3e06fd392be0edfe595aecd9a17372d74c6d4cb9 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:12:37 -0500 Subject: [PATCH 22/54] docs Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 172 +++++++++++++++--- .../clones/ERC721MagicDropCloneable.sol | 8 +- .../clones/ERC721MagicDropCloneable.t.sol | 34 ++-- 3 files changed, 168 insertions(+), 46 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 86f3277..3d8c89c 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -2,19 +2,31 @@ pragma solidity ^0.8.22; import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; +import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; import {ERC1155MagicDropMetadataCloneable} from "./ERC1155MagicDropMetadataCloneable.sol"; import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedCloneable.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; - contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, ReentrancyGuard { /// @dev Address that receives the primary sale proceeds of minted tokens. /// Configurable by the owner. If unset, withdrawals may fail. address internal _payoutRecipient; + /// @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 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 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 Configuration of the public mint stage, including timing and price. /// @notice Public mints occur only if the current timestamp is within [startTime, endTime]. mapping(uint256 => PublicStage) internal _publicStages; // tokenId => publicStage @@ -51,7 +63,13 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra = PUBLIC WRITE METHODS = ==============================================================*/ - function mintPublic(uint256 tokenId, uint256 qty, address to, bytes memory data) external payable nonReentrant { + /// @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 to The recipient address for the minted tokens. + /// @param tokenId The ID of the token to mint. + /// @param qty The number of tokens to mint. + function mintPublic(address to, uint256 tokenId, uint256 qty, bytes memory data) external payable nonReentrant { PublicStage memory stage = _publicStages[tokenId]; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); @@ -73,7 +91,18 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra _mint(to, tokenId, qty, data); } - function mintAllowlist(uint256 tokenId, uint256 qty, address to, bytes32[] calldata proof, bytes memory data) external payable nonReentrant { + /// @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 to The recipient address for the minted tokens. + /// @param tokenId The ID of the token to mint. + /// @param qty The number of tokens to mint. + /// @param proof The Merkle proof verifying `to` is eligible for the allowlist. + function mintAllowlist(address to, uint256 tokenId, uint256 qty, bytes32[] calldata proof, bytes memory data) + external + payable + nonReentrant + { AllowlistStage memory stage = _allowlistStages[tokenId]; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert AllowlistStageNotActive(); @@ -96,19 +125,36 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra _mint(to, tokenId, qty, data); } + /// @notice Burns a specific quantity of tokens from a given address. + /// @dev Reduces the total supply and calls the internal `_burn` function. + /// @param tokenId The ID of the token to burn. + /// @param qty The quantity of tokens to burn. + /// @param from The address from which the tokens will be burned. function burn(uint256 tokenId, uint256 qty, address from) external { _reduceSupplyOnBurn(tokenId, qty); _burn(from, tokenId, qty); } + /// @notice Burns a specific quantity of tokens on behalf of a given address. + /// @dev Reduces the total supply and calls the internal `_burn` function. + /// @param by The address initiating the burn. Must be an approved operator or the owner of the tokens. + /// @param from The address from which the tokens will be burned. + /// @param id The ID of the token to burn. + /// @param qty The quantity of tokens to burn. function burn(address by, address from, uint256 id, uint256 qty) external { _reduceSupplyOnBurn(id, qty); _burn(by, from, id, qty); } - function burnBatch(address by, address from, uint256[] calldata ids, uint256[] calldata qty) external { + /// @notice Burns multiple types of tokens in a single batch operation. + /// @dev Iterates over each token ID and quantity to reduce supply and burn tokens. + /// @param by The address initiating the batch burn. + /// @param from The address from which the tokens will be burned. + /// @param ids An array of token IDs to burn. + /// @param qty An array of quantities corresponding to each token ID to burn. + function batchBurn(address by, address from, uint256[] calldata ids, uint256[] calldata qty) external { uint256 length = ids.length; - for (uint256 i = 0; i < length; i++) { + for (uint256 i = 0; i < length;) { _reduceSupplyOnBurn(ids[i], qty[i]); unchecked { ++i; @@ -122,12 +168,6 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra = PUBLIC VIEW METHODS = ==============================================================*/ - /// @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 Returns the current public stage configuration (startTime, endTime, price). /// @return The current public stage settings. function getPublicStage(uint256 tokenId) external view returns (PublicStage memory) { @@ -140,6 +180,12 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra return _allowlistStages[tokenId]; } + /// @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 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. @@ -157,6 +203,9 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra = ADMIN OPERATIONS = ==============================================================*/ + /// @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. + /// @param config A struct containing all setup parameters. function setup(SetupConfig calldata config) external onlyOwner { if (config.maxSupply > 0) { _setMaxSupply(config.tokenId, config.maxSupply); @@ -187,7 +236,35 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra } } + /// @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(uint256 tokenId, PublicStage calldata stage) external onlyOwner { + _setPublicStage(tokenId, 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(uint256 tokenId, AllowlistStage calldata stage) external onlyOwner { + _setAllowlistStage(tokenId, 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 { + _setPayoutRecipient(newPayoutRecipient); + } + + /*============================================================== + = 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(uint256 tokenId, PublicStage calldata stage) internal { if (stage.startTime >= stage.endTime) { revert InvalidStageTime(); } @@ -202,28 +279,60 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra _publicStages[tokenId] = stage; } - function setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) external onlyOwner { + /// @notice Internal function to set the allowlist mint stage configuration. + /// @dev Reverts if timing is invalid or conflicts with the public stage. + /// @param tokenId The ID of the token to set the allowlist stage for. + /// @param stage A struct defining allowlist stage timings, price, and merkle root. + function _setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) internal { + if (stage.startTime >= stage.endTime) { + revert InvalidStageTime(); + } + + // Ensure the public stage starts after the allowlist stage ends + if (_publicStages[tokenId].startTime != 0 && _publicStages[tokenId].endTime != 0) { + if (stage.endTime > _publicStages[tokenId].startTime) { + revert InvalidAllowlistStageTime(); + } + } + _allowlistStages[tokenId] = stage; } - function setPayoutRecipient(address newPayoutRecipient) external onlyOwner { - _setPayoutRecipient(newPayoutRecipient); + /// @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) internal { + _payoutRecipient = newPayoutRecipient; } - /*============================================================== - = INTERNAL HELPERS = - ==============================================================*/ + /// @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(); + } - function _splitProceeds() internal {} + uint256 protocolFee = (msg.value * PROTOCOL_FEE_BPS) / BPS_DENOMINATOR; - function _setPayoutRecipient(address newPayoutRecipient) internal { - _payoutRecipient = newPayoutRecipient; - } + /// @dev Remaining balance is the balance minus the protocol fee. + uint256 remainingBalance; + unchecked { + remainingBalance = msg.value - protocolFee; + } - function _setPublicStage(uint256 tokenId, PublicStage calldata stage) internal {} + /// @dev Transfer the protocol fee to the protocol fee recipient. + SafeTransferLib.safeTransferETH(PROTOCOL_FEE_RECIPIENT, protocolFee); - function _setAllowlistStage(uint256 tokenId, AllowlistStage calldata stage) internal {} + /// @dev Transfer the remaining balance to the payout recipient. + SafeTransferLib.safeTransferETH(_payoutRecipient, remainingBalance); + } + /// @notice Internal function to reduce the total supply when tokens are burned. + /// @dev Decreases the `totalSupply` for a given `tokenId` by the specified `qty`. + /// Uses `unchecked` to save gas, assuming that underflow is impossible + /// because burn operations should not exceed the current supply. + /// @param tokenId The ID of the token being burned. + /// @param qty The quantity of tokens to burn. function _reduceSupplyOnBurn(uint256 tokenId, uint256 qty) internal { TokenSupply storage supply = _tokenSupply[tokenId]; unchecked { @@ -231,6 +340,15 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra } } + /// @notice Internal function to increase the total supply when tokens are minted. + /// @dev Increases the `totalSupply` and `totalMinted` for a given `tokenId` by the specified `qty`. + /// Ensures that the new total minted amount does not exceed the `maxSupply`. + /// Uses `unchecked` to save gas, assuming that overflow is impossible + /// because the maximum values are constrained by `maxSupply`. + /// @param to The address receiving the minted tokens. + /// @param tokenId The ID of the token being minted. + /// @param qty The quantity of tokens to mint. + /// @custom:reverts {CannotExceedMaxSupply} If the minting would exceed the maximum supply for the `tokenId`. function _increaseSupplyOnMint(address to, uint256 tokenId, uint256 qty) internal { TokenSupply storage supply = _tokenSupply[tokenId]; @@ -241,7 +359,6 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra unchecked { supply.totalSupply += uint64(qty); supply.totalMinted += uint64(qty); - _totalMintedByUserPerToken[to][tokenId] += uint64(qty); } } @@ -250,6 +367,9 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra = META = ==============================================================*/ + /// @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 ("ERC1155MagicDropCloneable", "1.0.0"); } @@ -258,6 +378,8 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra = MISC = ==============================================================*/ + /// @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; } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 3f40af8..a13b5a3 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 nonReentrant { + /// @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/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index daf1a2e..44fa99f 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); @@ -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 96e5a6bf09733f4faa33a2648814cdd06e7c9d7f Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:20:10 -0500 Subject: [PATCH 23/54] 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 24/54] 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 bf7f1949eec6974effc200174577cc728feddb9b Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:23:19 -0500 Subject: [PATCH 25/54] fix Signed-off-by: Adam Wolf --- .../nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 9c98eaa..39eb33c 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -77,7 +77,6 @@ contract ERC721MagicDropMetadataCloneable is /// @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; } From f85d80e9dc4bcf07afb901b52b5c2d3be876eeca Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:23:37 -0500 Subject: [PATCH 26/54] fix Signed-off-by: Adam Wolf --- contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol index 822c905..bdcbab6 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 dedfe0832e1969de8c0bc7996cdebec4669da383 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:53:07 -0500 Subject: [PATCH 27/54] natspec Signed-off-by: Adam Wolf --- .../interfaces/IERC1155MagicDropMetadata.sol | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol index 694e024..7f5005a 100644 --- a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol +++ b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol @@ -5,32 +5,87 @@ import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; interface IERC1155MagicDropMetadata is IMagicDropMetadata { struct TokenSupply { + /// @notice The maximum number of tokens that can be minted. uint64 maxSupply; + /// @notice The total number of tokens minted minus the number of tokens burned. uint64 totalSupply; + /// @notice The total number of tokens minted. uint64 totalMinted; } - error MaxSupplyCannotBeGreaterThan2ToThe64thPower(); + /*============================================================== + = EVENTS = + ==============================================================*/ - event MaxSupplyUpdated(uint256 tokenId, uint256 oldMaxSupply, uint256 newMaxSupply); + /// @notice Emitted when the max supply is updated. + /// @param _tokenId The token ID. + /// @param _oldMaxSupply The old max supply. + /// @param _newMaxSupply The new max supply. + event MaxSupplyUpdated(uint256 _tokenId, uint256 _oldMaxSupply, uint256 _newMaxSupply); - error WalletLimitExceeded(uint256 tokenId); + /// @notice Emitted when the wallet limit is updated. + /// @param _tokenId The token ID. + /// @param _walletLimit The new wallet limit. + event WalletLimitUpdated(uint256 _tokenId, uint256 _walletLimit); - event WalletLimitUpdated(uint256 tokenId, uint256 newWalletLimit); + /*============================================================== + = ERRORS = + ==============================================================*/ - function setMaxSupply(uint256 tokenId, uint256 newMaxSupply) external; + /// @notice Thrown when the max supply is greater than 2^64. + error MaxSupplyCannotBeGreaterThan2ToThe64thPower(); + + /// @notice Thrown when a mint would exceed the wallet-specific minting limit. + /// @param _tokenId The token ID. + error WalletLimitExceeded(uint256 _tokenId); - function setWalletLimit(uint256 tokenId, uint256 newWalletLimit) external; + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ - function name() external view returns (string memory); + /// @notice Returns the name of the token + function name(uint256 tokenId) external view returns (string memory); - function symbol() external view returns (string memory); + /// @notice Returns the symbol of the token + function symbol(uint256 tokenId) 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 + /// @param tokenId The ID of the token + /// @return The maximum supply cap for the collection function maxSupply(uint256 tokenId) external view returns (uint256); + /// @notice Returns the total number of tokens minted minus the number of tokens burned + /// @param tokenId The ID of the token + /// @return The total number of tokens minted minus the number of tokens burned function totalSupply(uint256 tokenId) external view returns (uint256); + /// @notice Returns the total number of tokens minted + /// @param tokenId The ID of the token + /// @return The total number of tokens minted function totalMinted(uint256 tokenId) 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 + /// @param tokenId The ID of the token + /// @return The maximum number of tokens allowed per wallet address function walletLimit(uint256 tokenId) external view returns (uint256); + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @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 tokenId The ID of the token. + /// @param newMaxSupply The new maximum number of tokens that can be minted + function setMaxSupply(uint256 tokenId, uint256 newMaxSupply) 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 tokenId The ID of the token. + /// @param walletLimit The new maximum number of tokens allowed per wallet + function setWalletLimit(uint256 tokenId, uint256 walletLimit) external; } From c6202a0172b15804cb27781a24566e360546e08e Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 11:56:44 -0500 Subject: [PATCH 28/54] docs Signed-off-by: Adam Wolf --- .../ERC1155ConduitPreapprovedCloneable.sol | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol index ce573da..f4286ff 100644 --- a/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol @@ -3,20 +3,34 @@ pragma solidity ^0.8.22; import {ERC1155} from "solady/src/tokens/ERC1155.sol"; -/// @title ERC1155ConduitPreapprovedCloneable -/// @notice ERC1155 with the MagicEden conduit preapproved. +/// @title ERC1155ConduitPreapprovedCloneable +/// @notice ERC1155 token with the MagicEden conduit preapproved for seamless transactions. abstract contract ERC1155ConduitPreapprovedCloneable is ERC1155 { - /// @dev The canonical MagicEden conduit. + /// @dev The canonical MagicEden conduit address. address internal constant _CONDUIT = 0x2052f8A2Ff46283B30084e5d84c89A2fdBE7f74b; - function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) - public - virtual - override - { + /// @notice Safely transfers `amount` tokens of type `id` from `from` to `to`. + /// @param from The address holding the tokens. + /// @param to The address to transfer the tokens to. + /// @param id The token type identifier. + /// @param amount The number of tokens to transfer. + /// @param data Additional data with no specified format. + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) public virtual override { _safeTransfer(_by(), from, to, id, amount, data); } + /// @notice Safely transfers a batch of tokens from `from` to `to`. + /// @param from The address holding the tokens. + /// @param to The address to transfer the tokens to. + /// @param ids An array of token type identifiers. + /// @param amounts An array of amounts to transfer for each token type. + /// @param data Additional data with no specified format. function safeBatchTransferFrom( address from, address to, @@ -27,11 +41,18 @@ abstract contract ERC1155ConduitPreapprovedCloneable is ERC1155 { _safeBatchTransfer(_by(), from, to, ids, amounts, data); } + /// @notice Checks if `operator` is approved to manage all of `owner`'s tokens. + /// @param owner The address owning the tokens. + /// @param operator The address to query for approval. + /// @return True if `operator` is approved, otherwise false. function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { if (operator == _CONDUIT) return true; return ERC1155.isApprovedForAll(owner, operator); } + /// @dev Determines the address initiating the transfer. + /// If the caller is the predefined conduit, returns address(0), else returns the caller's address. + /// @return result The address initiating the transfer. function _by() internal view virtual returns (address result) { assembly { // `msg.sender == _CONDUIT ? address(0) : msg.sender`. From 27bbe2967c3db172c02fd40460534507da0a930a Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 14:31:47 -0500 Subject: [PATCH 29/54] 1155 tests Signed-off-by: Adam Wolf --- .../ERC1155ConduitPreapprovedCloneable.sol | 12 +- .../clones/ERC1155MagicDropCloneable.sol | 22 +- .../interfaces/IERC1155MagicDropMetadata.sol | 4 +- .../ERC721MagicDropMetadataCloneable.sol | 2 +- .../interfaces/IERC721MagicDropMetadata.sol | 2 +- .../clones/ERC1155MagicDropCloneable.t.sol | 421 ++++++++++++++++++ .../ERC1155MagicDropMetadataCloneable.t.sol | 1 + .../clones/ERC721MagicDropCloneable.t.sol | 40 +- test/helpers/MerkleTestHelper.sol | 37 ++ 9 files changed, 492 insertions(+), 49 deletions(-) create mode 100644 test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol create mode 100644 test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol create mode 100644 test/helpers/MerkleTestHelper.sol diff --git a/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol index f4286ff..514fea0 100644 --- a/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155ConduitPreapprovedCloneable.sol @@ -15,13 +15,11 @@ abstract contract ERC1155ConduitPreapprovedCloneable is ERC1155 { /// @param id The token type identifier. /// @param amount The number of tokens to transfer. /// @param data Additional data with no specified format. - function safeTransferFrom( - address from, - address to, - uint256 id, - uint256 amount, - bytes calldata data - ) public virtual override { + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) + public + virtual + override + { _safeTransfer(_by(), from, to, id, amount, data); } diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 3d8c89c..6542a3d 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -59,6 +59,19 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra /// @notice Thrown when the payout recipient is set to a zero address. error PayoutRecipientCannotBeZeroAddress(); + /*============================================================== + = INITIALIZERS = + ==============================================================*/ + + /// @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-1155 name of the collection. + /// @param _symbol The ERC-1155 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 { + __ERC1155MagicDropMetadataCloneable__init(_name, _symbol, _owner); + } + /*============================================================== = PUBLIC WRITE METHODS = ==============================================================*/ @@ -88,6 +101,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra _splitProceeds(); } + _increaseSupplyOnMint(to, tokenId, qty); _mint(to, tokenId, qty, data); } @@ -117,6 +131,10 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra revert NotEnoughValue(); } + if (_totalMintedByUserPerToken[to][tokenId] + qty > this.walletLimit(tokenId)) { + revert WalletLimitExceeded(tokenId); + } + if (stage.price != 0) { _splitProceeds(); } @@ -127,10 +145,10 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra /// @notice Burns a specific quantity of tokens from a given address. /// @dev Reduces the total supply and calls the internal `_burn` function. + /// @param from The address from which the tokens will be burned. /// @param tokenId The ID of the token to burn. /// @param qty The quantity of tokens to burn. - /// @param from The address from which the tokens will be burned. - function burn(uint256 tokenId, uint256 qty, address from) external { + function burn(address from, uint256 tokenId, uint256 qty) external { _reduceSupplyOnBurn(tokenId, qty); _burn(from, tokenId, qty); } diff --git a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol index 7f5005a..c5c9cca 100644 --- a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol +++ b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol @@ -44,10 +44,10 @@ interface IERC1155MagicDropMetadata is IMagicDropMetadata { ==============================================================*/ /// @notice Returns the name of the token - function name(uint256 tokenId) external view returns (string memory); + function name() external view returns (string memory); /// @notice Returns the symbol of the token - function symbol(uint256 tokenId) external view returns (string memory); + function symbol() 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 diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 39eb33c..3f557e7 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -74,7 +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) { 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 +} diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol new file mode 100644 index 0000000..ad2472d --- /dev/null +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -0,0 +1,421 @@ +// 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 {MerkleTestHelper} from "test/helpers/MerkleTestHelper.sol"; + +import {ERC1155MagicDropCloneable} from "contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol"; +import {PublicStage, AllowlistStage, SetupConfig} from "contracts/nft/erc1155m/clones/Types.sol"; +import {IERC1155MagicDropMetadata} from "contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol"; + +contract ERC1155MagicDropCloneableTest is Test { + ERC1155MagicDropCloneable public token; + MerkleTestHelper public merkleHelper; + + 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; + uint256 internal allowlistStart; + uint256 internal allowlistEnd; + + uint256 internal tokenId = 1; + + SetupConfig internal config; + + function setUp() public { + token = ERC1155MagicDropCloneable(LibClone.deployERC1967(address(new ERC1155MagicDropCloneable()))); + merkleHelper = new MerkleTestHelper(allowedAddr); + + // Initialize token + token.initialize("TestToken", "TT", owner); + + // Default stages + allowlistStart = block.timestamp + 100; + allowlistEnd = block.timestamp + 200; + + publicStart = block.timestamp + 300; + publicEnd = block.timestamp + 400; + + config = SetupConfig({ + tokenId: tokenId, + maxSupply: 1000, + walletLimit: 5, + baseURI: "https://example.com/metadata/", + contractURI: "https://example.com/contract-metadata.json", + allowlistStage: AllowlistStage({ + 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 + }); + + vm.prank(owner); + token.setup(config); + } + + /*============================================================== + = TEST INITIALIZATION / SETUP = + ==============================================================*/ + + function testInitialization() public { + assertEq(token.owner(), owner); + assertEq(token.name(), "TestToken"); + assertEq(token.symbol(), "TT"); + } + + function testReinitializeReverts() 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(publicStart + 1); + + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + + assertEq(token.balanceOf(user, tokenId), 1); + } + + function testMintPublicBeforeStartReverts() public { + // Before start + vm.warp(publicStart - 10); + vm.deal(user, 1 ether); + + vm.prank(user); + vm.expectRevert(ERC1155MagicDropCloneable.PublicStageNotActive.selector); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + } + + function testMintPublicAfterEndReverts() public { + // After end + vm.warp(publicEnd + 10); + vm.deal(user, 1 ether); + + vm.prank(user); + vm.expectRevert(ERC1155MagicDropCloneable.PublicStageNotActive.selector); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + } + + function testMintPublicNotEnoughValueReverts() public { + vm.warp(publicStart + 1); + vm.deal(user, 0.005 ether); + + vm.prank(user); + vm.expectRevert(ERC1155MagicDropCloneable.NotEnoughValue.selector); + token.mintPublic{value: 0.005 ether}(user, tokenId, 1, ""); + } + + function testMintPublicWalletLimitExceededReverts() public { + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + + vm.startPrank(user); + // Mint up to the limit (5) + token.mintPublic{value: 0.05 ether}(user, tokenId, 5, ""); + assertEq(token.balanceOf(user, tokenId), 5); + + // Attempt to mint one more + vm.expectRevert(abi.encodeWithSelector(IERC1155MagicDropMetadata.WalletLimitExceeded.selector, tokenId)); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + vm.stopPrank(); + } + + /*============================================================== + = TEST ALLOWLIST MINTING STAGE = + ==============================================================*/ + + function testMintAllowlistHappyPath() public { + // Move time to allowlist + vm.warp(allowlistStart + 1); + + vm.deal(merkleHelper.getAllowedAddress(), 1 ether); + vm.prank(merkleHelper.getAllowedAddress()); + token.mintAllowlist{value: 0.005 ether}( + merkleHelper.getAllowedAddress(), tokenId, 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()), "" + ); + + assertEq(token.balanceOf(merkleHelper.getAllowedAddress(), tokenId), 1); + } + + function testMintAllowlistInvalidProofReverts() public { + vm.warp(allowlistStart + 1); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + + vm.deal(allowedAddr, 1 ether); + vm.prank(allowedAddr); + + vm.expectRevert(ERC1155MagicDropCloneable.InvalidProof.selector); + token.mintAllowlist{value: 0.005 ether}(user, tokenId, 1, proof, ""); + } + + function testMintAllowlistNotActiveReverts() public { + // Before allowlist start + vm.warp(allowlistStart - 10); + + address allowedAddr = merkleHelper.getAllowedAddress(); + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + vm.deal(allowedAddr, 1 ether); + vm.prank(allowedAddr); + + vm.expectRevert(ERC1155MagicDropCloneable.AllowlistStageNotActive.selector); + token.mintAllowlist{value: 0.005 ether}(allowedAddr, tokenId, 1, proof, ""); + } + + function testMintAllowlistNotEnoughValueReverts() public { + 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(ERC1155MagicDropCloneable.NotEnoughValue.selector); + token.mintAllowlist{value: 0.001 ether}(allowedAddr, tokenId, 1, proof, ""); + } + + function testMintAllowlistWalletLimitExceededReverts() public { + 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}(allowedAddr, tokenId, 5, proof, ""); + assertEq(token.balanceOf(allowedAddr, tokenId), 5); + + vm.expectRevert(abi.encodeWithSelector(IERC1155MagicDropMetadata.WalletLimitExceeded.selector, tokenId)); + token.mintAllowlist{value: 0.005 ether}(allowedAddr, tokenId, 1, proof, ""); + vm.stopPrank(); + } + + /*============================================================== + = BURNING = + ==============================================================*/ + + function testBurnHappyPath() public { + // Public mint first + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + + assertEq(token.balanceOf(user, tokenId), 1); + + vm.prank(user); + token.burn(user, tokenId, 1); + + assertEq(token.balanceOf(user, tokenId), 0); + } + + function testBurnInvalidTokenReverts() public { + vm.prank(user); + vm.expectRevert(); + token.burn(user, 9999, 1); // non-existent token + } + + function testBurnNotOwnerReverts() public { + // mint to user + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + + vm.prank(user); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + + vm.prank(user2); + vm.expectRevert(); + token.burn(user, tokenId, 1); + } + + /*============================================================== + = GETTERS = + ==============================================================*/ + + function testGetPublicStage() public { + PublicStage memory ps = token.getPublicStage(tokenId); + assertEq(ps.startTime, publicStart); + assertEq(ps.endTime, publicEnd); + assertEq(ps.price, 0.01 ether); + } + + function testGetAllowlistStage() public view { + AllowlistStage memory als = token.getAllowlistStage(tokenId); + assertEq(als.startTime, allowlistStart); + assertEq(als.endTime, allowlistEnd); + assertEq(als.price, 0.005 ether); + assertEq(als.merkleRoot, merkleHelper.getRoot()); + } + + function testPayoutRecipient() public { + assertEq(token.payoutRecipient(), payoutRecipient); + } + + /*============================================================== + = SUPPORTSINTERFACE = + ==============================================================*/ + + function testSupportsInterface() public view { + // Just checks a known supported interface + assertTrue(token.supportsInterface(type(IERC1155MagicDropMetadata).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(ERC1155MagicDropCloneable.InvalidStageTime.selector); + token.setPublicStage(tokenId, 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(ERC1155MagicDropCloneable.InvalidStageTime.selector); + token.setAllowlistStage(tokenId, invalidStage); + } + + function testSetPublicStageOverlapWithAllowlistReverts() public { + // 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(allowlistEnd + 150), + price: 0.01 ether + }); + + vm.prank(owner); + vm.expectRevert(ERC1155MagicDropCloneable.InvalidPublicStageTime.selector); + token.setPublicStage(tokenId, overlappingStage); + } + + function testSetAllowlistStageOverlapWithPublicReverts() public { + // Current public ends at publicEnd + // Try to set allowlist that ends before public ends + AllowlistStage memory overlappingStage = AllowlistStage({ + startTime: uint64(publicEnd - 50), + endTime: uint64(publicEnd + 10), + price: 0.005 ether, + merkleRoot: merkleHelper.getRoot() + }); + + vm.prank(owner); + vm.expectRevert(ERC1155MagicDropCloneable.InvalidAllowlistStageTime.selector); + token.setAllowlistStage(tokenId, overlappingStage); + } + + function testSetPayoutRecipient() public { + vm.prank(owner); + token.setPayoutRecipient(address(0x8888)); + assertEq(token.payoutRecipient(), address(0x8888)); + } + + /*============================================================== + = 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.01 ether}(user, tokenId, 1, ""); + + // 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); + } + + function testSplitProceedsWithZeroPrice() public { + // Check initial balances + uint256 initialProtocolBalance = token.PROTOCOL_FEE_RECIPIENT().balance; + uint256 initialPayoutBalance = payoutRecipient.balance; + + vm.prank(owner); + token.setPublicStage( + tokenId, 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}(user, tokenId, 1, ""); + + // Check balances after minting + assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance); + assertEq(payoutRecipient.balance, initialPayoutBalance); + } + + function testSplitProceedsAllowlist() public { + // Move to allowlist time + vm.warp(allowlistStart + 1); + + // Check initial balances + uint256 initialProtocolBalance = token.PROTOCOL_FEE_RECIPIENT().balance; + uint256 initialPayoutBalance = payoutRecipient.balance; + + vm.deal(merkleHelper.getAllowedAddress(), 1 ether); + vm.prank(merkleHelper.getAllowedAddress()); + token.mintAllowlist{value: 0.005 ether}( + merkleHelper.getAllowedAddress(), tokenId, 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()), "" + ); + + // Check balances after minting + uint256 expectedProtocolFee = (0.005 ether * token.PROTOCOL_FEE_BPS()) / token.BPS_DENOMINATOR(); + uint256 expectedPayout = 0.005 ether - expectedProtocolFee; + + assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance + expectedProtocolFee); + assertEq(payoutRecipient.balance, initialPayoutBalance + expectedPayout); + } +} diff --git a/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol @@ -0,0 +1 @@ + diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 44fa99f..6612daf 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -7,46 +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; - } -} - contract ERC721MagicDropCloneableTest is Test { ERC721MagicDropCloneable public token; MerkleTestHelper public merkleHelper; @@ -54,6 +21,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 +30,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); 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 ac16d170862cb6e3615c60843e479fd3edda69bf Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 15:31:27 -0500 Subject: [PATCH 30/54] initial tests Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 10 --------- .../clones/ERC1155MagicDropCloneable.t.sol | 21 +++++++++++++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 6542a3d..0d82263 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -143,16 +143,6 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra _mint(to, tokenId, qty, data); } - /// @notice Burns a specific quantity of tokens from a given address. - /// @dev Reduces the total supply and calls the internal `_burn` function. - /// @param from The address from which the tokens will be burned. - /// @param tokenId The ID of the token to burn. - /// @param qty The quantity of tokens to burn. - function burn(address from, uint256 tokenId, uint256 qty) external { - _reduceSupplyOnBurn(tokenId, qty); - _burn(from, tokenId, qty); - } - /// @notice Burns a specific quantity of tokens on behalf of a given address. /// @dev Reduces the total supply and calls the internal `_burn` function. /// @param by The address initiating the burn. Must be an approved operator or the owner of the tokens. diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index ad2472d..ccf84f7 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -228,7 +228,7 @@ contract ERC1155MagicDropCloneableTest is Test { assertEq(token.balanceOf(user, tokenId), 1); vm.prank(user); - token.burn(user, tokenId, 1); + token.burn(user, user, tokenId, 1); assertEq(token.balanceOf(user, tokenId), 0); } @@ -236,11 +236,10 @@ contract ERC1155MagicDropCloneableTest is Test { function testBurnInvalidTokenReverts() public { vm.prank(user); vm.expectRevert(); - token.burn(user, 9999, 1); // non-existent token + token.burn(user, user, 9999, 1); // non-existent token } function testBurnNotOwnerReverts() public { - // mint to user vm.warp(publicStart + 1); vm.deal(user, 1 ether); @@ -249,7 +248,21 @@ contract ERC1155MagicDropCloneableTest is Test { vm.prank(user2); vm.expectRevert(); - token.burn(user, tokenId, 1); + token.burn(user2, user, tokenId, 1); + } + + function testBurnFromAuthorizedNonOwner() public { + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + + vm.startPrank(user); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + token.setApprovalForAll(user2, true); + vm.stopPrank(); + + vm.prank(user2); + token.burn(user2, user, tokenId, 1); + assertEq(token.balanceOf(user, tokenId), 0); } /*============================================================== From 0dbc7b0842e2e4e9baebb2b1bc7f08eb5463bf37 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 16:09:50 -0500 Subject: [PATCH 31/54] update tests Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 4 +- .../clones/ERC721MagicDropCloneable.sol | 19 +++++----- .../ERC721MagicDropMetadataCloneable.sol | 12 +++--- .../clones/ERC1155MagicDropCloneable.t.sol | 31 +++++++++++++++ .../clones/ERC721MagicDropCloneable.t.sol | 38 +++++++++++++++++-- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 0d82263..70d2885 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -93,7 +93,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra revert NotEnoughValue(); } - if (_totalMintedByUserPerToken[to][tokenId] + qty > this.walletLimit(tokenId)) { + if (_walletLimit[tokenId] > 0 && _totalMintedByUserPerToken[to][tokenId] + qty > _walletLimit[tokenId]) { revert WalletLimitExceeded(tokenId); } @@ -131,7 +131,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra revert NotEnoughValue(); } - if (_totalMintedByUserPerToken[to][tokenId] + qty > this.walletLimit(tokenId)) { + if (_walletLimit[tokenId] > 0 && _totalMintedByUserPerToken[to][tokenId] + qty > _walletLimit[tokenId]) { revert WalletLimitExceeded(tokenId); } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index a13b5a3..6b8bd2d 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol @@ -108,10 +108,14 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert NotEnoughValue(); } - if (_numberMinted(to) + qty > this.walletLimit()) { + if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) { revert WalletLimitExceeded(); } + if (_totalMinted() + qty > _maxSupply) { + revert CannotExceedMaxSupply(); + } + if (stage.price != 0) { _splitProceeds(); } @@ -140,10 +144,14 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert NotEnoughValue(); } - if (_numberMinted(to) + qty > this.walletLimit()) { + if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) { revert WalletLimitExceeded(); } + if (_totalMinted() + qty > _maxSupply) { + revert CannotExceedMaxSupply(); + } + if (stage.price != 0) { _splitProceeds(); } @@ -368,11 +376,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; - } } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 3f557e7..6176d74 100644 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol @@ -39,25 +39,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 = diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index ccf84f7..8152e30 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -12,6 +12,7 @@ import {MerkleTestHelper} from "test/helpers/MerkleTestHelper.sol"; import {ERC1155MagicDropCloneable} from "contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "contracts/nft/erc1155m/clones/Types.sol"; import {IERC1155MagicDropMetadata} from "contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol"; +import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; contract ERC1155MagicDropCloneableTest is Test { ERC1155MagicDropCloneable public token; @@ -141,6 +142,19 @@ contract ERC1155MagicDropCloneableTest is Test { vm.stopPrank(); } + function testMintPublicMaxSupplyExceededReverts() public { + vm.warp(publicStart + 1); + vm.deal(user, 10.01 ether); + + vm.prank(owner); + // unlimited wallet limit for the purpose of this test + token.setWalletLimit(tokenId, 0); + + vm.prank(user); + vm.expectRevert(IMagicDropMetadata.CannotExceedMaxSupply.selector); + token.mintPublic{value: 10.01 ether}(user, tokenId, 1001, ""); + } + /*============================================================== = TEST ALLOWLIST MINTING STAGE = ==============================================================*/ @@ -213,6 +227,23 @@ contract ERC1155MagicDropCloneableTest is Test { vm.stopPrank(); } + function testMintAllowlistMaxSupplyExceededReverts() public { + vm.warp(allowlistStart + 1); + + vm.prank(owner); + // unlimited wallet limit for the purpose of this test + token.setWalletLimit(tokenId, 0); + + address allowedAddr = merkleHelper.getAllowedAddress(); + vm.deal(allowedAddr, 11 ether); + vm.prank(allowedAddr); + + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + + vm.expectRevert(IMagicDropMetadata.CannotExceedMaxSupply.selector); + token.mintAllowlist{value: 11 ether}(allowedAddr, tokenId, 1001, proof, ""); + } + /*============================================================== = BURNING = ==============================================================*/ diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 6612daf..0e974cd 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -13,6 +13,7 @@ import {ERC721MagicDropCloneable} from "contracts/nft/erc721m/clones/ERC721Magic 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"; +import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; contract ERC721MagicDropCloneableTest is Test { ERC721MagicDropCloneable public token; @@ -138,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 = ==============================================================*/ @@ -210,6 +224,24 @@ 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 = ==============================================================*/ @@ -222,7 +254,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); @@ -405,8 +437,8 @@ contract ERC721MagicDropCloneableTest is Test { 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 testTokenURIForNonexistentTokenReverts() public { From 2f4ddef2f6accd6d198861290b548eb9d7c52c94 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 17 Dec 2024 17:44:04 -0500 Subject: [PATCH 32/54] tests Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 12 ++-- .../clones/ERC721MagicDropCloneable.sol | 13 ++-- .../clones/ERC1155MagicDropCloneable.t.sol | 69 +++++++++++++++++++ .../clones/ERC721MagicDropCloneable.t.sol | 22 ++++++ 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 70d2885..67df307 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.22; import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; -import {ReentrancyGuard} from "solady/src/utils/ReentrancyGuard.sol"; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; import {ERC1155MagicDropMetadataCloneable} from "./ERC1155MagicDropMetadataCloneable.sol"; @@ -10,7 +9,7 @@ import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedClo import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; -contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, ReentrancyGuard { +contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @dev Address that receives the primary sale proceeds of minted tokens. /// Configurable by the owner. If unset, withdrawals may fail. address internal _payoutRecipient; @@ -82,7 +81,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra /// @param to The recipient address for the minted tokens. /// @param tokenId The ID of the token to mint. /// @param qty The number of tokens to mint. - function mintPublic(address to, uint256 tokenId, uint256 qty, bytes memory data) external payable nonReentrant { + function mintPublic(address to, uint256 tokenId, uint256 qty, bytes memory data) external payable { PublicStage memory stage = _publicStages[tokenId]; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { revert PublicStageNotActive(); @@ -97,11 +96,12 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra revert WalletLimitExceeded(tokenId); } + _increaseSupplyOnMint(to, tokenId, qty); + if (stage.price != 0) { _splitProceeds(); } - _increaseSupplyOnMint(to, tokenId, qty); _mint(to, tokenId, qty, data); } @@ -115,7 +115,6 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra function mintAllowlist(address to, uint256 tokenId, uint256 qty, bytes32[] calldata proof, bytes memory data) external payable - nonReentrant { AllowlistStage memory stage = _allowlistStages[tokenId]; if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { @@ -135,11 +134,12 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable, Reentra revert WalletLimitExceeded(tokenId); } + _increaseSupplyOnMint(to, tokenId, qty); + if (stage.price != 0) { _splitProceeds(); } - _increaseSupplyOnMint(to, tokenId, qty); _mint(to, tokenId, qty, data); } diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol index 6b8bd2d..8605185 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(); @@ -116,11 +115,11 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert CannotExceedMaxSupply(); } + _safeMint(to, qty); + if (stage.price != 0) { _splitProceeds(); } - - _safeMint(to, qty); } /// @notice Mints tokens during the allowlist stage. @@ -152,11 +151,11 @@ contract ERC721MagicDropCloneable is ERC721MagicDropMetadataCloneable, Reentranc revert CannotExceedMaxSupply(); } + _safeMint(to, qty); + if (stage.price != 0) { _splitProceeds(); } - - _safeMint(to, qty); } /// @notice Burns a specific token. diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index 8152e30..0bf2db5 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -332,6 +332,49 @@ contract ERC1155MagicDropCloneableTest is Test { = ADMIN OPERATIONS = ==============================================================*/ + function testSetupRevertsNotOwner() public { + vm.prank(user); + vm.expectRevert(); + token.setup(config); + } + + function testSetupEmptyConfigHasNoEffect() public { + vm.prank(owner); + token.setup(SetupConfig({ + tokenId: tokenId, + maxSupply: 0, + walletLimit: 0, + baseURI: "", + contractURI: "", + allowlistStage: AllowlistStage({ + startTime: uint64(0), + endTime: uint64(0), + price: 0, + merkleRoot: bytes32(0) + }), + publicStage: PublicStage({ + startTime: uint64(0), + endTime: uint64(0), + price: 0 + }), + payoutRecipient: address(0) + })); + + // check that the config has no effect because it's using the zero values + assertEq(token.maxSupply(tokenId), config.maxSupply); + assertEq(token.walletLimit(tokenId), config.walletLimit); + assertEq(token.baseURI(), config.baseURI); + assertEq(token.contractURI(), config.contractURI); + assertEq(token.getAllowlistStage(tokenId).startTime, config.allowlistStage.startTime); + assertEq(token.getAllowlistStage(tokenId).endTime, config.allowlistStage.endTime); + assertEq(token.getAllowlistStage(tokenId).price, config.allowlistStage.price); + assertEq(token.getAllowlistStage(tokenId).merkleRoot, config.allowlistStage.merkleRoot); + assertEq(token.getPublicStage(tokenId).startTime, config.publicStage.startTime); + assertEq(token.getPublicStage(tokenId).endTime, config.publicStage.endTime); + assertEq(token.getPublicStage(tokenId).price, config.publicStage.price); + assertEq(token.payoutRecipient(), config.payoutRecipient); + } + function testSetPublicStageInvalidTimesReverts() public { PublicStage memory invalidStage = PublicStage({ startTime: uint64(block.timestamp + 1000), @@ -462,4 +505,30 @@ contract ERC1155MagicDropCloneableTest is Test { assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance + expectedProtocolFee); assertEq(payoutRecipient.balance, initialPayoutBalance + expectedPayout); } + + 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(ERC1155MagicDropCloneable.PayoutRecipientCannotBeZeroAddress.selector); + token.mintPublic{value: 0.01 ether}(user, tokenId, 1, ""); + } + + /*============================================================== + = MISC = + ==============================================================*/ + + 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/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 0e974cd..499ed83 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -427,6 +427,21 @@ 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 = ==============================================================*/ @@ -445,4 +460,11 @@ contract ERC721MagicDropCloneableTest is Test { 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); + } } From 6a07d0118a054320b5e32acbc9dc1b06c5c76c73 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 12:04:24 -0500 Subject: [PATCH 33/54] tests Signed-off-by: Adam Wolf --- contracts/common/IMagicDropMetadata.sol | 85 ------- .../common/interfaces/IMagicDropMetadata.sol | 3 + .../ERC1155MagicDropMetadataCloneable.sol | 12 +- .../interfaces/IERC1155MagicDropMetadata.sol | 5 +- .../ERC721MagicDropMetadataCloneable.sol | 12 +- .../clones/ERC1155MagicDropCloneable.t.sol | 33 ++- .../ERC1155MagicDropMetadataCloneable.t.sol | 208 ++++++++++++++++++ .../clones/ERC721MagicDropCloneable.t.sol | 3 +- .../ERC721MagicDropMetadataCloneable.t.sol | 5 + 9 files changed, 251 insertions(+), 115 deletions(-) delete mode 100644 contracts/common/IMagicDropMetadata.sol diff --git a/contracts/common/IMagicDropMetadata.sol b/contracts/common/IMagicDropMetadata.sol deleted file mode 100644 index cddc0eb..0000000 --- a/contracts/common/IMagicDropMetadata.sol +++ /dev/null @@ -1,85 +0,0 @@ -// 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; -} diff --git a/contracts/common/interfaces/IMagicDropMetadata.sol b/contracts/common/interfaces/IMagicDropMetadata.sol index cddc0eb..5b5ba91 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/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol index 7bcb752..6819d0f 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol @@ -9,6 +9,9 @@ import {ERC1155} from "solady/src/tokens/ERC1155.sol"; import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedCloneable.sol"; +/// @title ERC1155MagicDropMetadataCloneable +/// @notice A cloneable ERC-1155 implementation that supports adjustable metadata URIs, royalty configuration. +/// Inherits conduit-based preapprovals, making distribution more gas-efficient. contract ERC1155MagicDropMetadataCloneable is ERC1155ConduitPreapprovedCloneable, IERC1155MagicDropMetadata, @@ -207,13 +210,20 @@ contract ERC1155MagicDropMetadataCloneable is emit BatchMetadataUpdate(0, type(uint256).max); } + /// @notice Internal function setting the contract URI. + /// @param newContractURI The new contract metadata URI. function _setContractURI(string calldata newContractURI) internal { _contractURI = newContractURI; } + /// @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(newReceiver, newBps); + emit RoyaltyInfoUpdated(newReceiver, newBps); } /// @notice Internal function setting the maximum token supply. @@ -226,7 +236,7 @@ contract ERC1155MagicDropMetadataCloneable is revert MaxSupplyCannotBeIncreased(); } - if (newMaxSupply < oldMaxSupply) { + if (newMaxSupply < _tokenSupply[tokenId].totalMinted) { revert MaxSupplyCannotBeLessThanCurrentSupply(); } diff --git a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol index c5c9cca..78e4cf9 100644 --- a/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol +++ b/contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; -import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; interface IERC1155MagicDropMetadata is IMagicDropMetadata { struct TokenSupply { @@ -32,9 +32,6 @@ interface IERC1155MagicDropMetadata is IMagicDropMetadata { = ERRORS = ==============================================================*/ - /// @notice Thrown when the max supply is greater than 2^64. - error MaxSupplyCannotBeGreaterThan2ToThe64thPower(); - /// @notice Thrown when a mint would exceed the wallet-specific minting limit. /// @param _tokenId The token ID. error WalletLimitExceeded(uint256 _tokenId); diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol index 6176d74..a3ec99c 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"; @@ -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); } diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index 0bf2db5..36c33b5 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -12,7 +12,7 @@ import {MerkleTestHelper} from "test/helpers/MerkleTestHelper.sol"; import {ERC1155MagicDropCloneable} from "contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "contracts/nft/erc1155m/clones/Types.sol"; import {IERC1155MagicDropMetadata} from "contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol"; -import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; contract ERC1155MagicDropCloneableTest is Test { ERC1155MagicDropCloneable public token; @@ -340,25 +340,18 @@ contract ERC1155MagicDropCloneableTest is Test { function testSetupEmptyConfigHasNoEffect() public { vm.prank(owner); - token.setup(SetupConfig({ - tokenId: tokenId, - maxSupply: 0, - walletLimit: 0, - baseURI: "", - contractURI: "", - allowlistStage: AllowlistStage({ - startTime: uint64(0), - endTime: uint64(0), - price: 0, - merkleRoot: bytes32(0) - }), - publicStage: PublicStage({ - startTime: uint64(0), - endTime: uint64(0), - price: 0 - }), - payoutRecipient: address(0) - })); + token.setup( + SetupConfig({ + tokenId: tokenId, + maxSupply: 0, + walletLimit: 0, + baseURI: "", + contractURI: "", + allowlistStage: AllowlistStage({startTime: uint64(0), endTime: uint64(0), price: 0, merkleRoot: bytes32(0)}), + publicStage: PublicStage({startTime: uint64(0), endTime: uint64(0), price: 0}), + payoutRecipient: address(0) + }) + ); // check that the config has no effect because it's using the zero values assertEq(token.maxSupply(tokenId), config.maxSupply); diff --git a/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol index 8b13789..ab58756 100644 --- a/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol @@ -1 +1,209 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; +import {Test} from "forge-std/Test.sol"; +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; + +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {LibClone} from "solady/src/utils/LibClone.sol"; +import {Initializable} from "solady/src/utils/Initializable.sol"; + +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; +import {IERC1155MagicDropMetadata} from "contracts/nft/erc1155m/interfaces/IERC1155MagicDropMetadata.sol"; +import {ERC1155MagicDropMetadataCloneable} from "contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.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 TestableERC1155MagicDropMetadataCloneable is ERC1155MagicDropMetadataCloneable { + bool private _initialized; + + function initialize(string memory name_, string memory symbol_, address owner_) external initializer { + __ERC1155MagicDropMetadataCloneable__init(name_, symbol_, owner_); + _initialized = true; + } + + function mintForTest(address to, uint256 tokenId, uint256 quantity) external onlyOwner { + _totalMintedByUserPerToken[to][tokenId] += quantity; + _tokenSupply[tokenId].totalMinted += uint64(quantity); + _tokenSupply[tokenId].totalSupply += uint64(quantity); + + _mint(to, tokenId, quantity, ""); + } +} + +contract ERC1155MagicDropMetadataCloneableTest is Test { + TestableERC1155MagicDropMetadataCloneable token; + + address owner = address(0x1234); + address user = address(0xABCD); + address royaltyReceiver = address(0x9999); + + uint256 internal constant TOKEN_ID = 1; + uint256 internal constant TOKEN_ID_2 = 2; + + function setUp() public { + token = TestableERC1155MagicDropMetadataCloneable( + LibClone.deployERC1967(address(new TestableERC1155MagicDropMetadataCloneable())) + ); + token.initialize("Test Collection", "TST", owner); + } + + /*============================================================== + = INITIALIZATION = + ==============================================================*/ + + function testInitialization() public view { + assertEq(token.owner(), owner); + assertEq(token.name(), "Test Collection"); + assertEq(token.symbol(), "TST"); + assertEq(token.baseURI(), ""); + assertEq(token.contractURI(), ""); + // maxSupply, walletLimit, and royalty not set for tokenId yet + assertEq(token.maxSupply(TOKEN_ID), 0); + assertEq(token.walletLimit(TOKEN_ID), 0); + assertEq(token.royaltyAddress(), address(0)); + assertEq(token.royaltyBps(), 0); + } + + function testCannotInitializeTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + token.initialize("Test Collection", "TST", owner); + } + + /*============================================================== + = 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"); + + // setMaxSupply + vm.prank(user); + vm.expectRevert(Ownable.Unauthorized.selector); + token.setMaxSupply(TOKEN_ID, 1000); + + // setWalletLimit + vm.prank(user); + vm.expectRevert(Ownable.Unauthorized.selector); + token.setWalletLimit(TOKEN_ID, 20); + + // setRoyaltyInfo + vm.prank(user); + vm.expectRevert(Ownable.Unauthorized.selector); + token.setRoyaltyInfo(royaltyReceiver, 500); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + function testSetBaseURI() public { + vm.prank(owner); + token.setBaseURI("https://example.com/metadata/"); + assertEq(token.baseURI(), "https://example.com/metadata/"); + } + + function testSetContractURI() public { + vm.prank(owner); + token.setContractURI("https://new-contract-uri.json"); + assertEq(token.contractURI(), "https://new-contract-uri.json"); + } + + function testSetMaxSupplyBasic() public { + vm.startPrank(owner); + token.setMaxSupply(TOKEN_ID, 1000); + vm.stopPrank(); + assertEq(token.maxSupply(TOKEN_ID), 1000); + } + + function testSetMaxSupplyDecreaseNotBelowMinted() public { + vm.startPrank(owner); + token.mintForTest(user, TOKEN_ID, 10); + // Currently minted = 10 + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); + token.setMaxSupply(TOKEN_ID, 5); + + // Setting exactly to 10 should pass if we first set initial max supply + token.setMaxSupply(TOKEN_ID, 1000); // set initial max supply + token.setMaxSupply(TOKEN_ID, 10); // now decrease to minted + assertEq(token.maxSupply(TOKEN_ID), 10); + vm.stopPrank(); + } + + function testSetMaxSupplyCannotIncreaseBeyondOriginal() public { + vm.startPrank(owner); + token.setMaxSupply(TOKEN_ID, 1000); + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeIncreased.selector); + token.setMaxSupply(TOKEN_ID, 2000); + vm.stopPrank(); + } + + function testSetMaxSupplyToCurrentSupply() public { + vm.startPrank(owner); + token.mintForTest(user, TOKEN_ID, 10); + token.setMaxSupply(TOKEN_ID, 10); + assertEq(token.maxSupply(TOKEN_ID), 10); + vm.stopPrank(); + } + + function testSetWalletLimit() public { + vm.startPrank(owner); + token.setWalletLimit(TOKEN_ID, 20); + assertEq(token.walletLimit(TOKEN_ID), 20); + vm.stopPrank(); + } + + function testSetZeroWalletLimit() public { + vm.startPrank(owner); + token.setWalletLimit(TOKEN_ID, 0); + assertEq(token.walletLimit(TOKEN_ID), 0); + vm.stopPrank(); + } + + function testSetRoyaltyInfo() public { + vm.startPrank(owner); + token.setRoyaltyInfo(royaltyReceiver, 500); + + assertEq(token.royaltyAddress(), royaltyReceiver); + assertEq(token.royaltyBps(), 500); + + // Check ERC2981 royaltyInfo + (address receiver, uint256 amount) = IERC2981(address(token)).royaltyInfo(TOKEN_ID, 10_000); + assertEq(receiver, royaltyReceiver); + assertEq(amount, 500); // 5% of 10000 = 500 + vm.stopPrank(); + } + + function testSetRoyaltyInfoZeroAddress() public { + vm.startPrank(owner); + vm.expectRevert(); + token.setRoyaltyInfo(address(0), 1000); + vm.stopPrank(); + } + + /*============================================================== + = SUPPORTS INTERFACE = + ==============================================================*/ + + function testSupportsInterface() public view { + // ERC2981 interfaceId = 0x2a55205a + assertTrue(token.supportsInterface(0x2a55205a)); + // ERC4906 interfaceId = 0x49064906 + assertTrue(token.supportsInterface(0x49064906)); + // ERC1155 interfaceId = 0xd9b67a26 + assertTrue(token.supportsInterface(0xd9b67a26)); + // Some random interface + assertFalse(token.supportsInterface(0x12345678)); + } +} diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 499ed83..6100568 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -13,7 +13,7 @@ import {ERC721MagicDropCloneable} from "contracts/nft/erc721m/clones/ERC721Magic 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"; -import {IMagicDropMetadata} from "contracts/common/IMagicDropMetadata.sol"; +import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; contract ERC721MagicDropCloneableTest is Test { ERC721MagicDropCloneable public token; @@ -241,7 +241,6 @@ contract ERC721MagicDropCloneableTest is Test { token.mintAllowlist{value: 11 ether}(allowedAddr, 1001, proof); } - /*============================================================== = BURNING = ==============================================================*/ diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol index 5a61d64..fd4bf28 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)); } From ae607199f62c3011ec6405e2f5c32978b66a8dea Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 12:50:58 -0500 Subject: [PATCH 34/54] test coverage Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.t.sol | 26 +++++++++++- .../ERC1155MagicDropMetadataCloneable.t.sol | 40 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index 36c33b5..41d3d9d 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -296,6 +296,30 @@ contract ERC1155MagicDropCloneableTest is Test { assertEq(token.balanceOf(user, tokenId), 0); } + function testBatchBurn() public { + vm.warp(publicStart + 1); + vm.deal(user, 1 ether); + + vm.startPrank(user); + token.mintPublic{value: 0.05 ether}(user, tokenId, 5, ""); + token.setApprovalForAll(user2, true); + vm.stopPrank(); + + assertEq(token.balanceOf(user, tokenId), 5); + assertEq(token.totalSupply(tokenId), 5); + + uint256[] memory ids = new uint256[](1); + ids[0] = tokenId; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 2; + + vm.prank(user); + token.batchBurn(user, user, ids, amounts); + assertEq(token.balanceOf(user, tokenId), 3); + assertEq(token.totalSupply(tokenId), 3); + assertEq(token.totalMinted(tokenId), 5); + } + /*============================================================== = GETTERS = ==============================================================*/ @@ -491,10 +515,10 @@ contract ERC1155MagicDropCloneableTest is Test { merkleHelper.getAllowedAddress(), tokenId, 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()), "" ); - // Check balances after minting uint256 expectedProtocolFee = (0.005 ether * token.PROTOCOL_FEE_BPS()) / token.BPS_DENOMINATOR(); uint256 expectedPayout = 0.005 ether - expectedProtocolFee; + // Check balances after minting assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance + expectedProtocolFee); assertEq(payoutRecipient.balance, initialPayoutBalance + expectedPayout); } diff --git a/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol index ab58756..1d02976 100644 --- a/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropMetadataCloneable.t.sol @@ -64,6 +64,8 @@ contract ERC1155MagicDropMetadataCloneableTest is Test { // maxSupply, walletLimit, and royalty not set for tokenId yet assertEq(token.maxSupply(TOKEN_ID), 0); assertEq(token.walletLimit(TOKEN_ID), 0); + assertEq(token.totalSupply(TOKEN_ID), 0); + assertEq(token.totalMinted(TOKEN_ID), 0); assertEq(token.royaltyAddress(), address(0)); assertEq(token.royaltyBps(), 0); } @@ -157,6 +159,20 @@ contract ERC1155MagicDropMetadataCloneableTest is Test { vm.stopPrank(); } + function testMintIncreasesTotalSupply() public { + vm.startPrank(owner); + token.mintForTest(user, TOKEN_ID, 10); + assertEq(token.totalSupply(TOKEN_ID), 10); + vm.stopPrank(); + } + + function testMintIncreasesTotalMinted() public { + vm.startPrank(owner); + token.mintForTest(user, TOKEN_ID, 10); + assertEq(token.totalMinted(TOKEN_ID), 10); + vm.stopPrank(); + } + function testSetWalletLimit() public { vm.startPrank(owner); token.setWalletLimit(TOKEN_ID, 20); @@ -192,8 +208,30 @@ contract ERC1155MagicDropMetadataCloneableTest is Test { vm.stopPrank(); } + function testSetURI() public { + vm.startPrank(owner); + token.setBaseURI("https://example.com/metadata/"); + assertEq(token.uri(TOKEN_ID), "https://example.com/metadata/"); + vm.stopPrank(); + } + + function testEmitBatchMetadataUpdate() public { + vm.startPrank(owner); + vm.expectEmit(true, true, true, true); + emit IMagicDropMetadata.BatchMetadataUpdate(TOKEN_ID, 10); + token.emitBatchMetadataUpdate(TOKEN_ID, 10); + vm.stopPrank(); + } + + function testMaxSupplyCannotBeGreaterThan2ToThe64thPower() public { + vm.startPrank(owner); + vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeGreaterThan2ToThe64thPower.selector); + token.setMaxSupply(TOKEN_ID, 2 ** 64); + vm.stopPrank(); + } + /*============================================================== - = SUPPORTS INTERFACE = + = METADATA TESTS = ==============================================================*/ function testSupportsInterface() public view { From 46686fa6faa56516a92265ec5a902662143e2acc Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 12:58:45 -0500 Subject: [PATCH 35/54] tests Signed-off-by: Adam Wolf --- .../clones/ERC721MagicDropCloneable.t.sol | 23 ++++++++++++++++++- .../ERC721MagicDropMetadataCloneable.t.sol | 6 +++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol index 6100568..a188619 100644 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol @@ -446,7 +446,6 @@ contract ERC721MagicDropCloneableTest is Test { ==============================================================*/ function testTokenURI() public { - // Mint token #1 vm.warp(publicStart + 1); vm.deal(user, 1 ether); vm.prank(user); @@ -455,6 +454,28 @@ contract ERC721MagicDropCloneableTest is Test { 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); diff --git a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol index fd4bf28..779f95e 100644 --- a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol +++ b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol @@ -247,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); + } } From e5ac06363cf8d18fa610eb11392dbf0893710605 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 13:01:25 -0500 Subject: [PATCH 36/54] 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 37/54] 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 4574a2be39e9410b3e848f9df29e358bf862628f Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 13:16:10 -0500 Subject: [PATCH 38/54] update Signed-off-by: Adam Wolf --- contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 67df307..93dc4d1 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -279,7 +279,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { // Ensure the public stage starts after the allowlist stage ends if (_allowlistStages[tokenId].startTime != 0 && _allowlistStages[tokenId].endTime != 0) { - if (stage.startTime < _allowlistStages[tokenId].endTime) { + if (stage.startTime <= _allowlistStages[tokenId].endTime) { revert InvalidPublicStageTime(); } } @@ -298,7 +298,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { // Ensure the public stage starts after the allowlist stage ends if (_publicStages[tokenId].startTime != 0 && _publicStages[tokenId].endTime != 0) { - if (stage.endTime > _publicStages[tokenId].startTime) { + if (stage.endTime >= _publicStages[tokenId].startTime) { revert InvalidAllowlistStageTime(); } } From fb00eab71cd6c113c6ce4795583a248153549c6d Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 13:17:20 -0500 Subject: [PATCH 39/54] 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 01483c22f85b49880b02d705168a6ba8da87e327 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 14:47:04 -0500 Subject: [PATCH 40/54] cleanup Signed-off-by: Adam Wolf --- .../nft/erc721m/clones/ERC721ACloneable.sol | 1292 ----------------- .../ERC721AConduitPreapprovedCloneable.sol | 29 - .../clones/ERC721AQueryableCloneable.sol | 237 --- .../clones/ERC721MagicDropCloneable.sol | 378 ----- .../ERC721MagicDropMetadataCloneable.sol | 232 --- contracts/nft/erc721m/clones/Types.sol | 43 - 6 files changed, 2211 deletions(-) delete mode 100644 contracts/nft/erc721m/clones/ERC721ACloneable.sol delete mode 100644 contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol delete mode 100644 contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol delete mode 100644 contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol delete mode 100644 contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol delete mode 100644 contracts/nft/erc721m/clones/Types.sol diff --git a/contracts/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol deleted file mode 100644 index 401fde7..0000000 --- a/contracts/nft/erc721m/clones/ERC721ACloneable.sol +++ /dev/null @@ -1,1292 +0,0 @@ -// 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; - - // ============================================================= - // INITIALIZER - // ============================================================= - - function __ERC721ACloneable__init(string memory name_, string memory symbol_) internal onlyInitializing { - _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] == 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 == 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 == 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. - _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 == 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]) == 0) --tokenId; - result = packed & _BITMASK_BURNED == 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(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) - // 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`. - */ - function _getApprovedSlotAndAddress(uint256 tokenId) - private - view - returns (uint256 approvedAddressSlot, address approvedAddress) - { - TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; - // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. - assembly { - approvedAddressSlot := tokenApproval.slot - approvedAddress := 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); - - // 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); - } - - (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); - } - } - - _beforeTokenTransfers(from, to, tokenId, 1); - - // Clear approvals from the previous owner. - assembly { - if approvedAddress { - // This is equivalent to `delete _tokenApprovals[tokenId]`. - sstore(approvedAddressSlot, 0) - } - } - - // 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 == 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] == 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` 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. - from, // `from`. - toMasked, // `to`. - tokenId // `tokenId`. - ) - } - if (toMasked == 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 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 == 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 == 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` to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; - - if (toMasked == 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 == 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` to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; - - if (toMasked == 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); - - address from = address(uint160(prevOwnershipPacked)); - - (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); - - 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); - } - } - } - - _beforeTokenTransfers(from, address(0), tokenId, 1); - - // Clear approvals from the previous owner. - assembly { - if approvedAddress { - // This is equivalent to `delete _tokenApprovals[tokenId]`. - sstore(approvedAddressSlot, 0) - } - } - - // 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 == 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] == 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++; - } - } - - // ============================================================= - // 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 == 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; - } - - // ============================================================= - // 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) - } - } -} diff --git a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol deleted file mode 100644 index 5fa7f9b..0000000 --- a/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -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. -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(ERC721ACloneable, IERC721A) - returns (bool) - { - if (operator == _CONDUIT) { - return true; - } - - return ERC721ACloneable.isApprovedForAll(owner, operator); - } -} diff --git a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol deleted file mode 100644 index 194af04..0000000 --- a/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol +++ /dev/null @@ -1,237 +0,0 @@ -// 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) - } - } - } - } -} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol deleted file mode 100644 index a154490..0000000 --- a/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol +++ /dev/null @@ -1,378 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.22; - -import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; - -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 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 { - /*============================================================== - = STORAGE = - ==============================================================*/ - - /// @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 address that receives protocol fees on withdrawal. - /// @notice This is fixed and cannot be changed. - address public constant PROTOCOL_FEE_RECIPIENT = 0xA3833016a4eC61f5c253D71c77522cC8A1cC1106; - - /// @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 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 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 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; - - /*============================================================== - = ERRORS = - ==============================================================*/ - - /// @notice Thrown when attempting to mint during a public stage that is not currently active. - error PublicStageNotActive(); - - /// @notice Thrown when attempting to mint during an allowlist stage that is not currently active. - error AllowlistStageNotActive(); - - /// @notice Thrown when the provided ETH value for a mint is insufficient. - error NotEnoughValue(); - - /// @notice Thrown when the provided Merkle proof for an allowlist mint is invalid. - error InvalidProof(); - - /// @notice Thrown when a stage's start or end time configuration is invalid. - error InvalidStageTime(); - - /// @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(); - - /// @notice Thrown when the payout recipient is set to a zero address. - error PayoutRecipientCannotBeZeroAddress(); - - /*============================================================== - = INITIALIZERS = - ==============================================================*/ - - /// @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); - __ERC721MagicDropMetadataCloneable__init(_owner); - } - - /*============================================================== - = PUBLIC WRITE METHODS = - ==============================================================*/ - - /// @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 to The recipient address for the minted tokens. - /// @param qty The number of tokens to mint. - function mintPublic(address to, uint256 qty) external payable { - PublicStage memory stage = _publicStage; - if (block.timestamp < stage.startTime || block.timestamp > stage.endTime) { - revert PublicStageNotActive(); - } - - uint256 requiredPayment = stage.price * qty; - if (msg.value < requiredPayment) { - revert NotEnoughValue(); - } - - if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) { - revert WalletLimitExceeded(); - } - - if (_totalMinted() + qty > _maxSupply) { - revert CannotExceedMaxSupply(); - } - - _safeMint(to, qty); - - if (stage.price != 0) { - _splitProceeds(); - } - } - - /// @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 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(address to, uint256 qty, 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 requiredPayment = stage.price * qty; - if (msg.value < requiredPayment) { - revert NotEnoughValue(); - } - - if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) { - revert WalletLimitExceeded(); - } - - if (_totalMinted() + qty > _maxSupply) { - revert CannotExceedMaxSupply(); - } - - _safeMint(to, qty); - - if (stage.price != 0) { - _splitProceeds(); - } - } - - /// @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); - } - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @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 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 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 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 - view - virtual - override(ERC721MagicDropMetadataCloneable) - returns (bool) - { - return interfaceId == type(IERC721MagicDropMetadata).interfaceId || super.supportsInterface(interfaceId); - } - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @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. - /// @param config A struct containing all setup parameters. - function setup(SetupConfig calldata config) external onlyOwner { - if (config.maxSupply > 0) { - _setMaxSupply(config.maxSupply); - } - - if (config.walletLimit > 0) { - _setWalletLimit(config.walletLimit); - } - - if (bytes(config.baseURI).length > 0) { - _setBaseURI(config.baseURI); - } - - if (bytes(config.contractURI).length > 0) { - _setContractURI(config.contractURI); - } - - if (config.allowlistStage.startTime != 0 || config.allowlistStage.endTime != 0) { - _setAllowlistStage(config.allowlistStage); - } - - if (config.publicStage.startTime != 0 || config.publicStage.endTime != 0) { - _setPublicStage(config.publicStage); - } - - if (config.payoutRecipient != address(0)) { - _setPayoutRecipient(config.payoutRecipient); - } - } - - /// @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; - } - - /*============================================================== - = 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(); - } - - // Ensure the public stage starts after the allowlist stage ends - if (_allowlistStage.startTime != 0 && _allowlistStage.endTime != 0) { - if (stage.startTime <= _allowlistStage.endTime) { - revert InvalidPublicStageTime(); - } - } - - _publicStage = stage; - } - - /// @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(); - } - - // Ensure the public stage starts after the allowlist stage ends - if (_publicStage.startTime != 0 && _publicStage.endTime != 0) { - if (stage.endTime >= _publicStage.startTime) { - revert InvalidAllowlistStageTime(); - } - } - - _allowlistStage = stage; - } - - /// @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) internal { - _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 = - ==============================================================*/ - - /// @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 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 - virtual - override(ERC721ACloneable, IERC721A) - returns (string memory) - { - if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); - - string memory baseURI = _baseURI(); - bool isBaseURIEmpty = bytes(baseURI).length == 0; - bool hasNoTrailingSlash = !isBaseURIEmpty && bytes(baseURI)[bytes(baseURI).length - 1] != bytes("/")[0]; - - if (isBaseURIEmpty) { - return ""; - } - if (hasNoTrailingSlash) { - return baseURI; - } - - return string(abi.encodePacked(baseURI, _toString(tokenId))); - } - - /*============================================================== - = MISC = - ==============================================================*/ - - /// @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; - } -} diff --git a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol deleted file mode 100644 index 717aaa8..0000000 --- a/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol +++ /dev/null @@ -1,232 +0,0 @@ -// 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"; - -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"; - -/// @title ERC721MagicDropMetadataCloneable -/// @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, - 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); - - emit MagicDropTokenDeployed(); - } - - /*============================================================== - = STORAGE = - ==============================================================*/ - - /// @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 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 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 internal _maxSupply; - - /// @notice The per-wallet minting limit, restricting how many tokens a single address can mint. - uint256 internal _walletLimit; - - /// @notice The address receiving royalty payments. - address internal _royaltyReceiver; - - /// @notice The royalty amount (in basis points) for secondary sales (e.g., 100 = 1%). - uint96 internal _royaltyBps; - - /*============================================================== - = PUBLIC VIEW METHODS = - ==============================================================*/ - - /// @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 _tokenBaseURI; - } - - /// @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 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 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 The address designated to receive royalty payments on secondary sales. - /// @return The royalty receiver address. - function royaltyAddress() public view returns (address) { - return _royaltyReceiver; - } - - /// @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 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 - virtual - override(ERC2981, ERC721ACloneable, IERC721A) - returns (bool) - { - return interfaceId == 0x2a55205a // ERC-2981 royalties - || interfaceId == 0x49064906 // ERC-4906 metadata updates - || ERC721ACloneable.supportsInterface(interfaceId); - } - - /*============================================================== - = ADMIN OPERATIONS = - ==============================================================*/ - - /// @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 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 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(); - } - - if (newMaxSupply < _totalMinted()) { - revert MaxSupplyCannotBeLessThanCurrentSupply(); - } - - if (newMaxSupply > 2 ** 64 - 1) { - revert MaxSupplyCannotBeGreaterThan2ToThe64thPower(); - } - - _maxSupply = newMaxSupply; - emit MaxSupplyUpdated(newMaxSupply); - } - - /// @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 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 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 deleted file mode 100644 index c1a7a97..0000000 --- a/contracts/nft/erc721m/clones/Types.sol +++ /dev/null @@ -1,43 +0,0 @@ -// 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; -} From a309ac7c92df72a757f7b8038c76f4824fce1936 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 18 Dec 2024 14:48:03 -0500 Subject: [PATCH 41/54] cleanup Signed-off-by: Adam Wolf --- .../interfaces/IERC721MagicDropMetadata.sol | 39 -- .../clones/ERC721MagicDropCloneable.t.sol | 490 ------------------ .../ERC721MagicDropMetadataCloneable.t.sol | 256 --------- 3 files changed, 785 deletions(-) delete mode 100644 contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol delete mode 100644 test/erc721m/clones/ERC721MagicDropCloneable.t.sol delete mode 100644 test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol deleted file mode 100644 index 822c905..0000000 --- a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.22; - -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 max supply is updated. - /// @param newMaxSupply The new max supply. - event MaxSupplyUpdated(uint256 newMaxSupply); - - /// @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 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 - /// 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 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; -} diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol deleted file mode 100644 index a188619..0000000 --- a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol +++ /dev/null @@ -1,490 +0,0 @@ -// 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 {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"; -import {IMagicDropMetadata} from "contracts/common/interfaces/IMagicDropMetadata.sol"; - -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 allowedAddr = address(0x3333); - address internal payoutRecipient = address(0x9999); - uint256 internal publicStart; - uint256 internal publicEnd; - uint256 internal allowlistStart; - uint256 internal allowlistEnd; - - function setUp() public { - token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable()))); - merkleHelper = new MerkleTestHelper(allowedAddr); - - // Initialize token - token.initialize("TestToken", "TT", owner); - - // Default stages - 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", - allowlistStage: AllowlistStage({ - 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 - }); - - 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(publicStart + 1); - - vm.deal(user, 1 ether); - - vm.prank(user); - token.mintPublic{value: 0.01 ether}(user, 1); - - assertEq(token.balanceOf(user), 1); - } - - function testMintPublicBeforeStartReverts() public { - // Before start - vm.warp(publicStart - 10); - vm.deal(user, 1 ether); - - vm.prank(user); - vm.expectRevert(ERC721MagicDropCloneable.PublicStageNotActive.selector); - token.mintPublic{value: 0.01 ether}(user, 1); - } - - function testMintPublicAfterEndReverts() public { - // After end - vm.warp(publicEnd + 10); - vm.deal(user, 1 ether); - - vm.prank(user); - vm.expectRevert(ERC721MagicDropCloneable.PublicStageNotActive.selector); - token.mintPublic{value: 0.01 ether}(user, 1); - } - - function testMintPublicNotEnoughValueReverts() public { - vm.warp(publicStart + 1); - vm.deal(user, 0.005 ether); - - vm.prank(user); - vm.expectRevert(ERC721MagicDropCloneable.NotEnoughValue.selector); - token.mintPublic{value: 0.005 ether}(user, 1); - } - - function testMintPublicWalletLimitExceededReverts() public { - vm.warp(publicStart + 1); - vm.deal(user, 1 ether); - - vm.startPrank(user); - // Mint up to the limit (5) - 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}(user, 1); - 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 = - ==============================================================*/ - - function testMintAllowlistHappyPath() public { - // Move time to allowlist - vm.warp(allowlistStart + 1); - - vm.deal(merkleHelper.getAllowedAddress(), 1 ether); - vm.prank(merkleHelper.getAllowedAddress()); - token.mintAllowlist{value: 0.005 ether}( - merkleHelper.getAllowedAddress(), 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()) - ); - - assertEq(token.balanceOf(merkleHelper.getAllowedAddress()), 1); - } - - function testMintAllowlistInvalidProofReverts() public { - 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}(user, 1, proof); - } - - function testMintAllowlistNotActiveReverts() public { - // Before allowlist start - 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}(allowedAddr, 1, proof); - } - - function testMintAllowlistNotEnoughValueReverts() public { - 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}(allowedAddr, 1, proof); - } - - function testMintAllowlistWalletLimitExceededReverts() public { - 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}(allowedAddr, 5, proof); - assertEq(token.balanceOf(allowedAddr), 5); - - vm.expectRevert(IERC721MagicDropMetadata.WalletLimitExceeded.selector); - token.mintAllowlist{value: 0.005 ether}(allowedAddr, 1, proof); - 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 = - ==============================================================*/ - - function testBurnHappyPath() public { - // Public mint first - vm.warp(publicStart + 1); - vm.deal(user, 1 ether); - - vm.prank(user); - token.mintPublic{value: 0.01 ether}(user, 1); - - uint256 tokenId = 0; - 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(publicStart + 1); - vm.deal(user, 1 ether); - - vm.prank(user); - token.mintPublic{value: 0.01 ether}(user, 1); - uint256 tokenId = 1; - - vm.prank(user2); - vm.expectRevert(); - token.burn(tokenId); - } - - /*============================================================== - = GETTERS = - ==============================================================*/ - - function testGetPublicStage() public { - PublicStage memory ps = token.getPublicStage(); - assertEq(ps.startTime, publicStart); - assertEq(ps.endTime, publicEnd); - assertEq(ps.price, 0.01 ether); - } - - function testGetAllowlistStage() public view { - AllowlistStage memory als = token.getAllowlistStage(); - assertEq(als.startTime, allowlistStart); - assertEq(als.endTime, allowlistEnd); - assertEq(als.price, 0.005 ether); - assertEq(als.merkleRoot, merkleHelper.getRoot()); - } - - function testPayoutRecipient() public { - assertEq(token.payoutRecipient(), payoutRecipient); - } - - /*============================================================== - = SUPPORTSINTERFACE = - ==============================================================*/ - - function testSupportsInterface() public view { - // 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 publicEnd+100 - // Try to set public stage that ends after that - PublicStage memory overlappingStage = PublicStage({ - startTime: uint64(block.timestamp + 10), - endTime: uint64(allowlistEnd + 150), - price: 0.01 ether - }); - - vm.prank(owner); - vm.expectRevert(ERC721MagicDropCloneable.InvalidPublicStageTime.selector); - token.setPublicStage(overlappingStage); - } - - function testSetAllowlistStageOverlapWithPublicReverts() public { - // Current public ends at publicEnd - // Try to set allowlist that ends before public ends - AllowlistStage memory overlappingStage = AllowlistStage({ - startTime: uint64(publicEnd - 50), - endTime: uint64(publicEnd + 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)); - } - - /*============================================================== - = 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.01 ether}(user, 1); - - // 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); - } - - function testSplitProceedsWithZeroPrice() public { - // Check initial balances - uint256 initialProtocolBalance = token.PROTOCOL_FEE_RECIPIENT().balance; - uint256 initialPayoutBalance = payoutRecipient.balance; - - vm.prank(owner); - 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}(user, 1); - - // Check balances after minting - assertEq(token.PROTOCOL_FEE_RECIPIENT().balance, initialProtocolBalance); - 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 { - 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(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 deleted file mode 100644 index 779f95e..0000000 --- a/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol +++ /dev/null @@ -1,256 +0,0 @@ -// 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 {IERC721A} from "erc721a/contracts/IERC721A.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); -} - -/// @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 view { - assertEq(token.owner(), owner); - assertEq(token.maxSupply(), 0); - assertEq(token.walletLimit(), 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 IMagicDropMetadata.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 IMagicDropMetadata.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 IMagicDropMetadata.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(IMagicDropMetadata.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(IMagicDropMetadata.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); - } - - /*============================================================== - = ROYALTY INFO = - ==============================================================*/ - - function testSetRoyaltyInfo() public { - vm.prank(owner); - vm.expectEmit(false, false, false, true); - emit IMagicDropMetadata.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 IMagicDropMetadata.BatchMetadataUpdate(2, 5); - token.emitBatchMetadataUpdate(2, 5); - vm.stopPrank(); - } - - /*============================================================== - = SUPPORTS INTERFACE = - ==============================================================*/ - - function testSupportsInterface() public view { - // ERC2981 interfaceId = 0x2a55205a - 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)); - } - - /*============================================================== - = 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(IMagicDropMetadata.MaxSupplyCannotBeLessThanCurrentSupply.selector); - token.setMaxSupply(1); - } - - 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); - } - - function testMaxSupplyCannotBeGreaterThan2ToThe64thPower() public { - vm.startPrank(owner); - vm.expectRevert(IMagicDropMetadata.MaxSupplyCannotBeGreaterThan2ToThe64thPower.selector); - token.setMaxSupply(2 ** 64); - } -} From fdcdcff21f4b5e93fd3955cf40b71782841d9ebe Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 19 Dec 2024 16:57:13 -0500 Subject: [PATCH 42/54] add royalties to setup config Signed-off-by: Adam Wolf --- .../erc1155m/clones/ERC1155MagicDropCloneable.sol | 4 ++++ contracts/nft/erc1155m/clones/Types.sol | 4 ++++ test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol | 12 ++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 93dc4d1..272695a 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -242,6 +242,10 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { 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/erc1155m/clones/Types.sol b/contracts/nft/erc1155m/clones/Types.sol index 25fd5c6..ac33d14 100644 --- a/contracts/nft/erc1155m/clones/Types.sol +++ b/contracts/nft/erc1155m/clones/Types.sol @@ -42,4 +42,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/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index 41d3d9d..b327874 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -27,6 +27,8 @@ contract ERC1155MagicDropCloneableTest is Test { uint256 internal publicEnd; uint256 internal allowlistStart; uint256 internal allowlistEnd; + address royaltyRecipient = address(0x8888); + uint96 royaltyBps = 1000; uint256 internal tokenId = 1; @@ -59,7 +61,9 @@ contract ERC1155MagicDropCloneableTest 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); @@ -373,7 +377,9 @@ contract ERC1155MagicDropCloneableTest is Test { contractURI: "", allowlistStage: AllowlistStage({startTime: uint64(0), endTime: uint64(0), price: 0, merkleRoot: bytes32(0)}), publicStage: PublicStage({startTime: uint64(0), endTime: uint64(0), price: 0}), - payoutRecipient: address(0) + payoutRecipient: address(0), + royaltyBps: 0, + royaltyRecipient: address(0) }) ); @@ -390,6 +396,8 @@ contract ERC1155MagicDropCloneableTest is Test { assertEq(token.getPublicStage(tokenId).endTime, config.publicStage.endTime); assertEq(token.getPublicStage(tokenId).price, config.publicStage.price); assertEq(token.payoutRecipient(), config.payoutRecipient); + assertEq(token.royaltyAddress(), config.royaltyRecipient); + assertEq(token.royaltyBps(), config.royaltyBps); } function testSetPublicStageInvalidTimesReverts() public { From 6a98ecdcf674a834ce79562ad78c7e9f2385b1f7 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 2 Jan 2025 18:08:09 -0500 Subject: [PATCH 43/54] add config function Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 19 +++++++++++++++++++ .../ERC1155MagicDropMetadataCloneable.sol | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 272695a..abdaa07 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -176,6 +176,25 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { = PUBLIC VIEW METHODS = ==============================================================*/ + /// @notice Returns the current configuration of the contract. + /// @return The current configuration of the contract. + function getConfig(uint256 tokenId) external view returns (SetupConfig memory) { + SetupConfig memory newConfig = SetupConfig({ + tokenId: tokenId, + maxSupply: _tokenSupply[tokenId].maxSupply, + walletLimit: _walletLimit[tokenId], + baseURI: _baseURI, + contractURI: _contractURI, + allowlistStage: _allowlistStages[tokenId], + publicStage: _publicStages[tokenId], + 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(uint256 tokenId) external view returns (PublicStage memory) { diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol index 6819d0f..1b6cdb2 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol @@ -35,7 +35,7 @@ contract ERC1155MagicDropMetadataCloneable is address internal _royaltyReceiver; /// @dev The royalty basis points. - uint256 internal _royaltyBps; + uint96 internal _royaltyBps; /// @dev The total supply of each token. mapping(uint256 => TokenSupply) internal _tokenSupply; From 2d9c3f9fe9b37e1e5102f59a517804859f5c1479 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 6 Jan 2025 17:49:42 -0500 Subject: [PATCH 44/54] emit contract updated event Signed-off-by: Adam Wolf --- .../nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol index 1b6cdb2..76f19fe 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropMetadataCloneable.sol @@ -214,6 +214,8 @@ contract ERC1155MagicDropMetadataCloneable is /// @param newContractURI The new contract metadata URI. function _setContractURI(string calldata newContractURI) internal { _contractURI = newContractURI; + + emit ContractURIUpdated(newContractURI); } /// @notice Internal function setting the royalty information. From 12567c8d6fe8e612243676489008a1dc83b399b0 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 7 Jan 2025 16:49:57 -0500 Subject: [PATCH 45/54] add header Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index abdaa07..30070db 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -9,6 +9,56 @@ import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedClo import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; +/// ........ +/// ..... .. ... +/// .. ..... .. .. +/// .. ... ..... .. .. +/// .. ...... .. ...... .. +/// .. ......... ......... .... +/// .... .. .. ... +/// ........ ......... .. +/// .. ... ... .. ......... +/// .. .......... .... .... ....... ........ +/// ....... .. .. ... .... ..... .. +/// ........ . ... .. .. +/// . ..... ........ .... .. +/// .. .. ... ........... ... ... +/// ....... .. ...... ... .. +/// ............ ... ........ .. .. +/// ... ..... .. .. .. .. .. ...... +/// .. ........ ... .. .. .. .... .... +/// ....... .. .. ...... ....... .. +/// .. ..... .. .... .. +/// .. .... ......... . .. .. +/// ... .... .. ......... . .. .. +/// .... .... .. ..... ...... ... +/// ..... .. ........ ... ... +/// ... .. .. .. ...... ..... .. +/// .. .... ... ... .. .. +/// .. .... .. .. .. +/// . ...... .. .. .. +/// .. ...................... .............. +/// .. ................ .... ... +/// . ... ........ +/// .. ... ...... .. +/// .. .... ...EMMY.... +/// .. .. ... .... .... .. +/// .. .. ..... .......... +/// ... .. ... ...... +/// ... .... .. .. +/// .. ..... ... +/// ..... .... ........ ... +/// ........ .. ..... .......... +/// .. ........ .. ..MAGIC..... . +/// .... .... .... ..EDEN.... +/// ..... . ... ...... +/// .. ....... .. +/// ..... ..... +/// .... +/// @title ERC1155MagicDropCloneable +/// @notice A cloneable ERC-1155 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 ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @dev Address that receives the primary sale proceeds of minted tokens. /// Configurable by the owner. If unset, withdrawals may fail. From d9d911f5baf19c3bfbffcbd33cfc931d90767cdd Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Wed, 8 Jan 2025 17:21:43 -0500 Subject: [PATCH 46/54] adjust fee config Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 30070db..a6c88dc 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -70,7 +70,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @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%. @@ -393,19 +393,17 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { 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); } /// @notice Internal function to reduce the total supply when tokens are burned. From 285b228e7cc03cce19fab2523dcc6d978d141949 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 14 Jan 2025 14:04:07 -0500 Subject: [PATCH 47/54] add missing event Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index a6c88dc..1dbf1dc 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -60,6 +60,10 @@ import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata /// @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 ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { + /*============================================================== + = STORAGE = + ==============================================================*/ + /// @dev Address that receives the primary sale proceeds of minted tokens. /// Configurable by the owner. If unset, withdrawals may fail. address internal _payoutRecipient; @@ -84,6 +88,23 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @notice Only addresses proven by a valid Merkle proof can mint during this stage. mapping(uint256 => AllowlistStage) internal _allowlistStages; // tokenId => allowlistStage + /*============================================================== + = EVENTS = + ==============================================================*/ + + /// @notice Emitted when the public mint stage is set. + event PublicStageSet(PublicStage stage); + + /// @notice Emitted when the allowlist mint stage is set. + event AllowlistStageSet(AllowlistStage stage); + + /// @notice Emitted when the payout recipient is set. + event PayoutRecipientSet(address newPayoutRecipient); + + /*============================================================== + = ERRORS = + ==============================================================*/ + /// @notice Thrown when attempting to mint during a public stage that is not currently active. error PublicStageNotActive(); @@ -358,6 +379,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } _publicStages[tokenId] = stage; + emit PublicStageSet(stage); } /// @notice Internal function to set the allowlist mint stage configuration. @@ -377,6 +399,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } _allowlistStages[tokenId] = stage; + emit AllowlistStageSet(stage); } /// @notice Internal function to set the payout recipient. @@ -384,6 +407,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @param newPayoutRecipient The address to receive the payout from mint proceeds. function _setPayoutRecipient(address newPayoutRecipient) internal { _payoutRecipient = newPayoutRecipient; + emit PayoutRecipientSet(newPayoutRecipient); } /// @notice Internal function to split the proceeds of a mint. From 65af906a5d2c5ab9071e5731fc598d5e16557598 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 14 Jan 2025 14:20:02 -0500 Subject: [PATCH 48/54] add mint event Signed-off-by: Adam Wolf --- contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 1dbf1dc..0295636 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -101,6 +101,9 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @notice Emitted when the payout recipient is set. event PayoutRecipientSet(address newPayoutRecipient); + /// @notice Emitted when a token is minted. + event TokenMinted(address indexed to, uint256 tokenId, uint256 qty); + /*============================================================== = ERRORS = ==============================================================*/ @@ -174,6 +177,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } _mint(to, tokenId, qty, data); + emit TokenMinted(to, tokenId, qty); } /// @notice Mints tokens during the allowlist stage. @@ -212,6 +216,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } _mint(to, tokenId, qty, data); + emit TokenMinted(to, tokenId, qty); } /// @notice Burns a specific quantity of tokens on behalf of a given address. From 94a4bd03c5f5f5af7195ba6b1fc9c8d94727ed96 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Tue, 14 Jan 2025 14:22:52 -0500 Subject: [PATCH 49/54] change order of mint Signed-off-by: Adam Wolf --- contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 0295636..929137b 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -172,11 +172,12 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { _increaseSupplyOnMint(to, tokenId, qty); + _mint(to, tokenId, qty, data); + if (stage.price != 0) { _splitProceeds(); } - _mint(to, tokenId, qty, data); emit TokenMinted(to, tokenId, qty); } From eb888e2150f43ab5f681e2b11bd449dbb624a297 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 16 Jan 2025 11:13:27 -0500 Subject: [PATCH 50/54] update allowlist Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 2 +- .../clones/ERC1155MagicDropCloneable.t.sol | 32 +-- test/helpers/MerkleTestHelper.sol | 184 ++++++++++++++++-- 3 files changed, 184 insertions(+), 34 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 929137b..914b73b 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -197,7 +197,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { revert AllowlistStageNotActive(); } - if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(abi.encodePacked(to)))) { + if (!MerkleProofLib.verify(proof, stage.merkleRoot, keccak256(bytes.concat(keccak256(abi.encode(to)))))) { revert InvalidProof(); } diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index b327874..ddc4cae 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -35,8 +35,13 @@ contract ERC1155MagicDropCloneableTest is Test { SetupConfig internal config; function setUp() public { + // Prepare an array of addresses for testing allowlist + address[] memory addresses = new address[](1); + addresses[0] = allowedAddr; + // Deploy the new MerkleTestHelper with multiple addresses + merkleHelper = new MerkleTestHelper(addresses); + token = ERC1155MagicDropCloneable(LibClone.deployERC1967(address(new ERC1155MagicDropCloneable()))); - merkleHelper = new MerkleTestHelper(allowedAddr); // Initialize token token.initialize("TestToken", "TT", owner); @@ -167,19 +172,20 @@ contract ERC1155MagicDropCloneableTest is Test { // Move time to allowlist vm.warp(allowlistStart + 1); - vm.deal(merkleHelper.getAllowedAddress(), 1 ether); - vm.prank(merkleHelper.getAllowedAddress()); - token.mintAllowlist{value: 0.005 ether}( - merkleHelper.getAllowedAddress(), tokenId, 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()), "" - ); + vm.deal(allowedAddr, 1 ether); + vm.prank(allowedAddr); - assertEq(token.balanceOf(merkleHelper.getAllowedAddress(), tokenId), 1); + // Generate a proof for the allowedAddr from our new MerkleTestHelper + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + + token.mintAllowlist{value: 0.005 ether}(allowedAddr, tokenId, 1, proof, ""); + + assertEq(token.balanceOf(allowedAddr, tokenId), 1); } function testMintAllowlistInvalidProofReverts() public { vm.warp(allowlistStart + 1); - address allowedAddr = merkleHelper.getAllowedAddress(); bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); vm.deal(allowedAddr, 1 ether); @@ -193,7 +199,6 @@ contract ERC1155MagicDropCloneableTest is Test { // Before allowlist start vm.warp(allowlistStart - 10); - address allowedAddr = merkleHelper.getAllowedAddress(); bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); vm.deal(allowedAddr, 1 ether); vm.prank(allowedAddr); @@ -205,7 +210,6 @@ contract ERC1155MagicDropCloneableTest is Test { function testMintAllowlistNotEnoughValueReverts() public { vm.warp(allowlistStart + 1); - address allowedAddr = merkleHelper.getAllowedAddress(); bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); vm.deal(allowedAddr, 0.001 ether); vm.prank(allowedAddr); @@ -217,7 +221,6 @@ contract ERC1155MagicDropCloneableTest is Test { function testMintAllowlistWalletLimitExceededReverts() public { vm.warp(allowlistStart + 1); - address allowedAddr = merkleHelper.getAllowedAddress(); bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); vm.deal(allowedAddr, 1 ether); @@ -238,7 +241,6 @@ contract ERC1155MagicDropCloneableTest is Test { // unlimited wallet limit for the purpose of this test token.setWalletLimit(tokenId, 0); - address allowedAddr = merkleHelper.getAllowedAddress(); vm.deal(allowedAddr, 11 ether); vm.prank(allowedAddr); @@ -517,10 +519,10 @@ contract ERC1155MagicDropCloneableTest is Test { uint256 initialProtocolBalance = token.PROTOCOL_FEE_RECIPIENT().balance; uint256 initialPayoutBalance = payoutRecipient.balance; - vm.deal(merkleHelper.getAllowedAddress(), 1 ether); - vm.prank(merkleHelper.getAllowedAddress()); + vm.deal(allowedAddr, 1 ether); + vm.prank(allowedAddr); token.mintAllowlist{value: 0.005 ether}( - merkleHelper.getAllowedAddress(), tokenId, 1, merkleHelper.getProofFor(merkleHelper.getAllowedAddress()), "" + allowedAddr, tokenId, 1, merkleHelper.getProofFor(allowedAddr), "" ); uint256 expectedProtocolFee = (0.005 ether * token.PROTOCOL_FEE_BPS()) / token.BPS_DENOMINATOR(); diff --git a/test/helpers/MerkleTestHelper.sol b/test/helpers/MerkleTestHelper.sol index dfcfeca..ee46afa 100644 --- a/test/helpers/MerkleTestHelper.sol +++ b/test/helpers/MerkleTestHelper.sol @@ -1,37 +1,185 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.22; -// Dummy merkle proof generation utilities for testing +/** + * @title MerkleTestHelper + * @dev This contract builds a Merkle tree from a list of addresses, stores the root, + * and provides a function to retrieve a Merkle proof for a given address. + * + * NOTE: Generating Merkle trees on-chain is gas-expensive, so this is typically + * done only in testing scenarios or for very short lists. + */ 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; + address[] internal _allowedAddrs; 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)); + /** + * @dev Constructor that takes in an array of addresses, builds a Merkle tree, and stores the root. + */ + constructor(address[] memory allowedAddresses) { + // Copy addresses to storage + for (uint256 i = 0; i < allowedAddresses.length; i++) { + _allowedAddrs.push(allowedAddresses[i]); + } + + // Build leaves from the addresses + bytes32[] memory leaves = _buildLeaves(_allowedAddrs); + + // Compute merkle root + _root = _computeMerkleRoot(leaves); } + /** + * @notice Returns the Merkle root of the addresses list. + */ function getRoot() external view returns (bytes32) { return _root; } + /** + * @notice Returns the Merkle proof for a given address. + * @dev If the address is not found or is not part of the _allowedAddrs array, + * this will return an empty array. + */ function getProofFor(address addr) external view returns (bytes32[] memory) { - if (addr == _allowedAddr) { - // Single-leaf tree: no proof necessary except empty array + // Find the index of the address in our stored list + (bool isInList, uint256 index) = _findAddressIndex(addr); + if (!isInList) { + // Return empty proof if address doesn't exist in the allowed list return new bytes32[](0); - } else { - // No valid proof - bytes32[] memory emptyProof; - return emptyProof; } + + // Build leaves in memory + bytes32[] memory leaves = _buildLeaves(_allowedAddrs); + + // Build the proof for the leaf at the found index + return _buildProof(leaves, index); + } + + /** + * @dev Creates an array of leaves by double hashing each address: + * keccak256(bytes.concat(keccak256(abi.encodePacked(address)))) + */ + function _buildLeaves(address[] memory addrs) internal pure returns (bytes32[] memory) { + bytes32[] memory leaves = new bytes32[](addrs.length); + for (uint256 i = 0; i < addrs.length; i++) { + leaves[i] = keccak256(bytes.concat(keccak256(abi.encode(addrs[i])))); + } + return leaves; + } + + /** + * @dev Computes the Merkle root from an array of leaves. + * Pairs each leaf, hashing them together until only one root remains. + * If there is an odd number of leaves at a given level, the last leaf is "promoted" (copied up). + */ + function _computeMerkleRoot(bytes32[] memory leaves) internal pure returns (bytes32) { + require(leaves.length > 0, "No leaves to build a merkle root"); + + uint256 n = leaves.length; + while (n > 1) { + for (uint256 i = 0; i < n / 2; i++) { + // Sort the pair before hashing + (bytes32 left, bytes32 right) = leaves[2 * i] < leaves[2 * i + 1] + ? (leaves[2 * i], leaves[2 * i + 1]) + : (leaves[2 * i + 1], leaves[2 * i]); + leaves[i] = keccak256(abi.encodePacked(left, right)); + } + // If odd, promote last leaf + if (n % 2 == 1) { + leaves[n / 2] = leaves[n - 1]; + n = (n / 2) + 1; + } else { + n = n / 2; + } + } + + // The first element is now the root + return leaves[0]; + } + + /** + * @dev Builds a Merkle proof for the leaf at the given index. + * We recompute the pairing tree on the fly, capturing the "sibling" each time. + */ + function _buildProof(bytes32[] memory leaves, uint256 targetIndex) internal pure returns (bytes32[] memory) { + bytes32[] memory proof = new bytes32[](_proofLength(leaves.length)); + uint256 proofPos = 0; + uint256 n = leaves.length; + uint256 index = targetIndex; + + while (n > 1) { + bool isIndexEven = (index % 2) == 0; + uint256 pairIndex = isIndexEven ? index + 1 : index - 1; + + if (pairIndex < n) { + // Add the sibling to the proof without sorting + proof[proofPos] = leaves[pairIndex]; + proofPos++; + } + + // Move up to the next level + for (uint256 i = 0; i < n / 2; i++) { + // Sort pairs when building the next level + (bytes32 left, bytes32 right) = leaves[2 * i] < leaves[2 * i + 1] + ? (leaves[2 * i], leaves[2 * i + 1]) + : (leaves[2 * i + 1], leaves[2 * i]); + leaves[i] = keccak256(abi.encodePacked(left, right)); + } + + // Handle odd number of leaves + if (n % 2 == 1) { + leaves[n / 2] = leaves[n - 1]; + n = (n / 2) + 1; + } else { + n = n / 2; + } + + index = index / 2; + } + + // Trim unused proof elements + uint256 trimSize = 0; + for (uint256 i = proof.length; i > 0; i--) { + if (proof[i - 1] != 0) { + break; + } + trimSize++; + } + + bytes32[] memory trimmedProof = new bytes32[](proof.length - trimSize); + for (uint256 i = 0; i < trimmedProof.length; i++) { + trimmedProof[i] = proof[i]; + } + + return trimmedProof; } - function getAllowedAddress() external view returns (address) { - return _allowedAddr; + /** + * @dev Helper to find the index of a given address in the _allowedAddrs array. + */ + function _findAddressIndex(address addr) internal view returns (bool, uint256) { + for (uint256 i = 0; i < _allowedAddrs.length; i++) { + if (_allowedAddrs[i] == addr) { + return (true, i); + } + } + return (false, 0); + } + + /** + * @dev Computes an upper bound for the proof length (worst-case). + * For n leaves, the maximum proof length is ~log2(n). + * Here we just do a simple upper bound for clarity. + */ + function _proofLength(uint256 n) internal pure returns (uint256) { + // If n=1, no proof. Otherwise, each tree level can contribute 1 node in the proof path. + // A simplistic approach: log2(n) <= 256 bits for typical usage, but we do this in-line: + uint256 count = 0; + while (n > 1) { + n = (n + 1) / 2; // integer division round up + count++; + } + return count; } } From e449030f4bd9a6143afbe33ad7168f4df19dc8a5 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 16 Jan 2025 11:22:01 -0500 Subject: [PATCH 51/54] fmt Signed-off-by: Adam Wolf --- test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index ddc4cae..82cb601 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -521,9 +521,7 @@ contract ERC1155MagicDropCloneableTest is Test { vm.deal(allowedAddr, 1 ether); vm.prank(allowedAddr); - token.mintAllowlist{value: 0.005 ether}( - allowedAddr, tokenId, 1, merkleHelper.getProofFor(allowedAddr), "" - ); + token.mintAllowlist{value: 0.005 ether}(allowedAddr, tokenId, 1, merkleHelper.getProofFor(allowedAddr), ""); uint256 expectedProtocolFee = (0.005 ether * token.PROTOCOL_FEE_BPS()) / token.BPS_DENOMINATOR(); uint256 expectedPayout = 0.005 ether - expectedProtocolFee; From d69dfbe2b66bbec2e9d7a1836080cb5b1c709c2c Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 20 Jan 2025 15:39:23 -0500 Subject: [PATCH 52/54] fix burn function for 1155 Signed-off-by: Adam Wolf --- .../nft/erc1155m/clones/ERC1155MagicDropCloneable.sol | 10 ++++------ lib/solady | 2 +- test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol | 10 +++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index 914b73b..b8a4d93 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -222,22 +222,20 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { /// @notice Burns a specific quantity of tokens on behalf of a given address. /// @dev Reduces the total supply and calls the internal `_burn` function. - /// @param by The address initiating the burn. Must be an approved operator or the owner of the tokens. /// @param from The address from which the tokens will be burned. /// @param id The ID of the token to burn. /// @param qty The quantity of tokens to burn. - function burn(address by, address from, uint256 id, uint256 qty) external { + function burn(address from, uint256 id, uint256 qty) external { _reduceSupplyOnBurn(id, qty); - _burn(by, from, id, qty); + _burn(msg.sender, from, id, qty); } /// @notice Burns multiple types of tokens in a single batch operation. /// @dev Iterates over each token ID and quantity to reduce supply and burn tokens. - /// @param by The address initiating the batch burn. /// @param from The address from which the tokens will be burned. /// @param ids An array of token IDs to burn. /// @param qty An array of quantities corresponding to each token ID to burn. - function batchBurn(address by, address from, uint256[] calldata ids, uint256[] calldata qty) external { + function batchBurn(address from, uint256[] calldata ids, uint256[] calldata qty) external { uint256 length = ids.length; for (uint256 i = 0; i < length;) { _reduceSupplyOnBurn(ids[i], qty[i]); @@ -246,7 +244,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } } - _batchBurn(by, from, ids, qty); + _batchBurn(msg.sender, from, ids, qty); } /*============================================================== diff --git a/lib/solady b/lib/solady index 362b2ef..6122858 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 362b2efd20f38aea7252b391e5e016633ff79641 +Subproject commit 6122858a3aed96ee9493b99f70a245237681a95f diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index 82cb601..82d4d07 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -265,7 +265,7 @@ contract ERC1155MagicDropCloneableTest is Test { assertEq(token.balanceOf(user, tokenId), 1); vm.prank(user); - token.burn(user, user, tokenId, 1); + token.burn(user, tokenId, 1); assertEq(token.balanceOf(user, tokenId), 0); } @@ -273,7 +273,7 @@ contract ERC1155MagicDropCloneableTest is Test { function testBurnInvalidTokenReverts() public { vm.prank(user); vm.expectRevert(); - token.burn(user, user, 9999, 1); // non-existent token + token.burn(user, 9999, 1); // non-existent token } function testBurnNotOwnerReverts() public { @@ -285,7 +285,7 @@ contract ERC1155MagicDropCloneableTest is Test { vm.prank(user2); vm.expectRevert(); - token.burn(user2, user, tokenId, 1); + token.burn(user, tokenId, 1); } function testBurnFromAuthorizedNonOwner() public { @@ -298,7 +298,7 @@ contract ERC1155MagicDropCloneableTest is Test { vm.stopPrank(); vm.prank(user2); - token.burn(user2, user, tokenId, 1); + token.burn(user, tokenId, 1); assertEq(token.balanceOf(user, tokenId), 0); } @@ -320,7 +320,7 @@ contract ERC1155MagicDropCloneableTest is Test { amounts[0] = 2; vm.prank(user); - token.batchBurn(user, user, ids, amounts); + token.batchBurn(user, ids, amounts); assertEq(token.balanceOf(user, tokenId), 3); assertEq(token.totalSupply(tokenId), 3); assertEq(token.totalMinted(tokenId), 5); From 2638278f954d50051d919df9965721f113accbd5 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 20 Jan 2025 16:03:54 -0500 Subject: [PATCH 53/54] remove unused imports Signed-off-by: Adam Wolf --- contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index b8a4d93..dc7e38c 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -5,7 +5,6 @@ import {MerkleProofLib} from "solady/src/utils/MerkleProofLib.sol"; import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; import {ERC1155MagicDropMetadataCloneable} from "./ERC1155MagicDropMetadataCloneable.sol"; -import {ERC1155ConduitPreapprovedCloneable} from "./ERC1155ConduitPreapprovedCloneable.sol"; import {PublicStage, AllowlistStage, SetupConfig} from "./Types.sol"; import {IERC1155MagicDropMetadata} from "../interfaces/IERC1155MagicDropMetadata.sol"; From 7dbd38085cb45d98b79271ad501b7144c0496476 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Mon, 20 Jan 2025 16:27:12 -0500 Subject: [PATCH 54/54] fix overpayment Signed-off-by: Adam Wolf --- .../clones/ERC1155MagicDropCloneable.sol | 10 +++---- .../clones/ERC1155MagicDropCloneable.t.sol | 28 ++++++++++++++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol index dc7e38c..8a55216 100644 --- a/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol +++ b/contracts/nft/erc1155m/clones/ERC1155MagicDropCloneable.sol @@ -114,7 +114,7 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { error AllowlistStageNotActive(); /// @notice Thrown when the provided ETH value for a mint is insufficient. - error NotEnoughValue(); + error RequiredValueNotMet(); /// @notice Thrown when the provided Merkle proof for an allowlist mint is invalid. error InvalidProof(); @@ -161,8 +161,8 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } uint256 requiredPayment = stage.price * qty; - if (msg.value < requiredPayment) { - revert NotEnoughValue(); + if (msg.value != requiredPayment) { + revert RequiredValueNotMet(); } if (_walletLimit[tokenId] > 0 && _totalMintedByUserPerToken[to][tokenId] + qty > _walletLimit[tokenId]) { @@ -201,8 +201,8 @@ contract ERC1155MagicDropCloneable is ERC1155MagicDropMetadataCloneable { } uint256 requiredPayment = stage.price * qty; - if (msg.value < requiredPayment) { - revert NotEnoughValue(); + if (msg.value != requiredPayment) { + revert RequiredValueNotMet(); } if (_walletLimit[tokenId] > 0 && _totalMintedByUserPerToken[to][tokenId] + qty > _walletLimit[tokenId]) { diff --git a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol index 82d4d07..762a753 100644 --- a/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol +++ b/test/erc1155m/clones/ERC1155MagicDropCloneable.t.sol @@ -132,7 +132,7 @@ contract ERC1155MagicDropCloneableTest is Test { vm.deal(user, 0.005 ether); vm.prank(user); - vm.expectRevert(ERC1155MagicDropCloneable.NotEnoughValue.selector); + vm.expectRevert(ERC1155MagicDropCloneable.RequiredValueNotMet.selector); token.mintPublic{value: 0.005 ether}(user, tokenId, 1, ""); } @@ -164,6 +164,16 @@ contract ERC1155MagicDropCloneableTest is Test { token.mintPublic{value: 10.01 ether}(user, tokenId, 1001, ""); } + function testMintPublicOverpayReverts() public { + vm.warp(publicStart + 1); + + vm.deal(user, 1 ether); + + vm.prank(user); + vm.expectRevert(ERC1155MagicDropCloneable.RequiredValueNotMet.selector); + token.mintPublic{value: 0.02 ether}(user, tokenId, 1, ""); + } + /*============================================================== = TEST ALLOWLIST MINTING STAGE = ==============================================================*/ @@ -214,7 +224,7 @@ contract ERC1155MagicDropCloneableTest is Test { vm.deal(allowedAddr, 0.001 ether); vm.prank(allowedAddr); - vm.expectRevert(ERC1155MagicDropCloneable.NotEnoughValue.selector); + vm.expectRevert(ERC1155MagicDropCloneable.RequiredValueNotMet.selector); token.mintAllowlist{value: 0.001 ether}(allowedAddr, tokenId, 1, proof, ""); } @@ -241,13 +251,23 @@ contract ERC1155MagicDropCloneableTest is Test { // unlimited wallet limit for the purpose of this test token.setWalletLimit(tokenId, 0); - vm.deal(allowedAddr, 11 ether); + vm.deal(allowedAddr, 5.005 ether); vm.prank(allowedAddr); bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); vm.expectRevert(IMagicDropMetadata.CannotExceedMaxSupply.selector); - token.mintAllowlist{value: 11 ether}(allowedAddr, tokenId, 1001, proof, ""); + token.mintAllowlist{value: 5.005 ether}(allowedAddr, tokenId, 1001, proof, ""); + } + + function testMintAllowlistOverpayReverts() public { + vm.warp(allowlistStart + 1); + + bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr); + vm.deal(allowedAddr, 1 ether); + + vm.expectRevert(ERC1155MagicDropCloneable.RequiredValueNotMet.selector); + token.mintAllowlist{value: 0.02 ether}(allowedAddr, tokenId, 1, proof, ""); } /*==============================================================