diff --git a/contracts/common/interfaces/IMagicDropMetadata.sol b/contracts/common/interfaces/IMagicDropMetadata.sol
new file mode 100644
index 00000000..5b5ba913
--- /dev/null
+++ b/contracts/common/interfaces/IMagicDropMetadata.sol
@@ -0,0 +1,88 @@
+// 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();
+
+ /// @notice Throw when the max supply is greater than 2^64.
+ error MaxSupplyCannotBeGreaterThan2ToThe64thPower();
+
+ /*==============================================================
+ = 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/nft/erc721m/clones/ERC721ACloneable.sol b/contracts/nft/erc721m/clones/ERC721ACloneable.sol
new file mode 100644
index 00000000..401fde71
--- /dev/null
+++ b/contracts/nft/erc721m/clones/ERC721ACloneable.sol
@@ -0,0 +1,1292 @@
+// 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
new file mode 100644
index 00000000..5fa7f9bd
--- /dev/null
+++ b/contracts/nft/erc721m/clones/ERC721AConduitPreapprovedCloneable.sol
@@ -0,0 +1,29 @@
+// 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
new file mode 100644
index 00000000..194af043
--- /dev/null
+++ b/contracts/nft/erc721m/clones/ERC721AQueryableCloneable.sol
@@ -0,0 +1,237 @@
+// 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
new file mode 100644
index 00000000..84156e83
--- /dev/null
+++ b/contracts/nft/erc721m/clones/ERC721MagicDropCloneable.sol
@@ -0,0 +1,465 @@
+// 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";
+
+/// ........
+/// ..... .. ...
+/// .. ..... .. ..
+/// .. ... ..... .. ..
+/// .. ...... .. ...... ..
+/// .. ......... ......... ....
+/// .... .. .. ...
+/// ........ ......... ..
+/// .. ... ... .. .........
+/// .. .......... .... .... ....... ........
+/// ....... .. .. ... .... ..... ..
+/// ........ . ... .. ..
+/// . ..... ........ .... ..
+/// .. .. ... ........... ... ...
+/// ....... .. ...... ... ..
+/// ............ ... ........ .. ..
+/// ... ..... .. .. .. .. .. ......
+/// .. ........ ... .. .. .. .... ....
+/// ....... .. .. ...... ....... ..
+/// .. ..... .. .... ..
+/// .. .... ......... . .. ..
+/// ... .... .. ......... . .. ..
+/// .... .... .. ..... ...... ...
+/// ..... .. ........ ... ...
+/// ... .. .. .. ...... ..... ..
+/// .. .... ... ... .. ..
+/// .. .... .. .. ..
+/// . ...... .. .. ..
+/// .. ...................... ..............
+/// .. ................ .... ...
+/// . ... ........
+/// .. ... ...... ..
+/// .. .... ...EMMY....
+/// .. .. ... .... .... ..
+/// .. .. ..... ..........
+/// ... .. ... ......
+/// ... .... .. ..
+/// .. ..... ...
+/// ..... .... ........ ...
+/// ........ .. ..... ..........
+/// .. ........ .. ..MAGIC..... .
+/// .... .... .... ..EDEN....
+/// ..... . ... ......
+/// .. ....... ..
+/// ..... .....
+/// ....
+/// @title ERC721MagicDropCloneable
+/// @notice A cloneable ERC-721A drop contract that supports both a public minting stage and an allowlist minting stage.
+/// @dev This contract extends metadata configuration, ownership, and royalty support from its parent, while adding
+/// 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 = 0; // 0%
+
+ /// @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;
+
+ /*==============================================================
+ = 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);
+
+ /// @notice Emitted when a token is minted.
+ event TokenMinted(address indexed to, uint256 tokenId, uint256 qty);
+
+ /*==============================================================
+ = 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 RequiredValueNotMet();
+
+ /// @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 RequiredValueNotMet();
+ }
+
+ if (_walletLimit > 0 && _numberMinted(to) + qty > _walletLimit) {
+ revert WalletLimitExceeded();
+ }
+
+ if (_totalMinted() + qty > _maxSupply) {
+ revert CannotExceedMaxSupply();
+ }
+
+ _safeMint(to, qty);
+
+ if (stage.price != 0) {
+ _splitProceeds();
+ }
+
+ emit TokenMinted(to, _totalMinted(), qty);
+ }
+
+ /// @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(bytes.concat(keccak256(abi.encode(to)))))) {
+ revert InvalidProof();
+ }
+
+ uint256 requiredPayment = stage.price * qty;
+ if (msg.value != requiredPayment) {
+ revert RequiredValueNotMet();
+ }
+
+ 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 configuration of the contract.
+ /// @return The current configuration of the contract.
+ function getConfig() external view returns (SetupConfig memory) {
+ SetupConfig memory newConfig = SetupConfig({
+ maxSupply: _maxSupply,
+ walletLimit: _walletLimit,
+ baseURI: _baseURI(),
+ contractURI: _contractURI,
+ allowlistStage: _allowlistStage,
+ publicStage: _publicStage,
+ payoutRecipient: _payoutRecipient,
+ royaltyRecipient: _royaltyReceiver,
+ royaltyBps: _royaltyBps
+ });
+
+ return newConfig;
+ }
+
+ /// @notice Returns the current public stage configuration (startTime, endTime, price).
+ /// @return The current public stage settings.
+ function getPublicStage() external view returns (PublicStage memory) {
+ 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);
+ }
+
+ if (config.royaltyRecipient != address(0)) {
+ _setRoyaltyInfo(config.royaltyRecipient, config.royaltyBps);
+ }
+ }
+
+ /// @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 {
+ _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(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;
+ emit PublicStageSet(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;
+ emit AllowlistStageSet(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;
+ emit PayoutRecipientSet(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();
+ }
+
+ 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);
+ }
+ }
+
+ /*==============================================================
+ = 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
new file mode 100644
index 00000000..ed94e3b9
--- /dev/null
+++ b/contracts/nft/erc721m/clones/ERC721MagicDropMetadataCloneable.sol
@@ -0,0 +1,231 @@
+// 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 {IERC721A} from "erc721a/contracts/IERC721A.sol";
+
+import {ERC721AConduitPreapprovedCloneable} from "./ERC721AConduitPreapprovedCloneable.sol";
+import {ERC721ACloneable} from "./ERC721ACloneable.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);
+
+ emit ContractURIUpdated(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
new file mode 100644
index 00000000..3c849341
--- /dev/null
+++ b/contracts/nft/erc721m/clones/Types.sol
@@ -0,0 +1,47 @@
+// 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 royalty recipient of the token.
+ address royaltyRecipient;
+ /// @dev The royalty basis points of the token.
+ uint96 royaltyBps;
+}
diff --git a/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol
new file mode 100644
index 00000000..822c9058
--- /dev/null
+++ b/contracts/nft/erc721m/interfaces/IERC721MagicDropMetadata.sol
@@ -0,0 +1,39 @@
+// 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/foundry.toml b/foundry.toml
index 1c70be40..2b323b6b 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/lib/solady b/lib/solady
index 362b2efd..6122858a 160000
--- a/lib/solady
+++ b/lib/solady
@@ -1 +1 @@
-Subproject commit 362b2efd20f38aea7252b391e5e016633ff79641
+Subproject commit 6122858a3aed96ee9493b99f70a245237681a95f
diff --git a/test/erc721m/clones/ERC721MagicDropCloneable.t.sol b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol
new file mode 100644
index 00000000..4c74a922
--- /dev/null
+++ b/test/erc721m/clones/ERC721MagicDropCloneable.t.sol
@@ -0,0 +1,510 @@
+// 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;
+ address royaltyRecipient = address(0x8888);
+ uint96 royaltyBps = 1000;
+
+ function setUp() public {
+ // Deploy a new token clone
+ token = ERC721MagicDropCloneable(LibClone.deployERC1967(address(new ERC721MagicDropCloneable())));
+
+ // 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);
+
+ // 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,
+ royaltyRecipient: royaltyRecipient,
+ royaltyBps: royaltyBps
+ });
+
+ vm.prank(owner);
+ token.setup(config);
+ }
+
+ /*==============================================================
+ = TEST INITIALIZATION =
+ ==============================================================*/
+
+ function testInitialization() public {
+ assertEq(token.owner(), owner);
+ assertEq(token.name(), "TestToken");
+ assertEq(token.symbol(), "TT");
+ }
+
+ 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.RequiredValueNotMet.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, 10.01 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: 10.01 ether}(user, 1001);
+ }
+
+ function testMintPublicOverpayReverts() public {
+ vm.warp(publicStart + 1);
+
+ vm.deal(user, 1 ether);
+
+ // Attempt to mint with excess Ether
+ vm.prank(user);
+ vm.expectRevert(ERC721MagicDropCloneable.RequiredValueNotMet.selector);
+ token.mintPublic{value: 0.02 ether}(user, 1);
+ }
+
+ /*==============================================================
+ = TEST ALLOWLIST MINTING STAGE =
+ ==============================================================*/
+
+ function testMintAllowlistHappyPath() public {
+ // Move time to allowlist
+ vm.warp(allowlistStart + 1);
+
+ vm.deal(allowedAddr, 1 ether);
+ vm.prank(allowedAddr);
+
+ // Generate a proof for the allowedAddr from our new MerkleTestHelper
+ bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr);
+
+ token.mintAllowlist{value: 0.005 ether}(allowedAddr, 1, proof);
+ assertEq(token.balanceOf(allowedAddr), 1);
+ }
+
+ function testMintAllowlistInvalidProofReverts() public {
+ vm.warp(allowlistStart + 1);
+
+ // Generate a proof for allowedAddr
+ bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr);
+
+ // We'll pass the minted tokens to a different address to invalidate the proof
+ 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);
+
+ 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);
+
+ bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr);
+
+ vm.deal(allowedAddr, 0.001 ether);
+ vm.prank(allowedAddr);
+
+ vm.expectRevert(ERC721MagicDropCloneable.RequiredValueNotMet.selector);
+ token.mintAllowlist{value: 0.001 ether}(allowedAddr, 1, proof);
+ }
+
+ function testMintAllowlistWalletLimitExceededReverts() public {
+ vm.warp(allowlistStart + 1);
+
+ 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);
+
+ bytes32[] memory proof = merkleHelper.getProofFor(allowedAddr);
+ vm.deal(allowedAddr, 5.005 ether);
+
+ vm.prank(allowedAddr);
+ vm.expectRevert(IMagicDropMetadata.CannotExceedMaxSupply.selector);
+ token.mintAllowlist{value: 5.005 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
new file mode 100644
index 00000000..779f95e1
--- /dev/null
+++ b/test/erc721m/clones/ERC721MagicDropMetadataCloneable.t.sol
@@ -0,0 +1,256 @@
+// 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);
+ }
+}
diff --git a/test/helpers/MerkleTestHelper.sol b/test/helpers/MerkleTestHelper.sol
new file mode 100644
index 00000000..ee46afac
--- /dev/null
+++ b/test/helpers/MerkleTestHelper.sol
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.22;
+
+/**
+ * @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 {
+ address[] internal _allowedAddrs;
+ bytes32 internal _root;
+
+ /**
+ * @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) {
+ // 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);
+ }
+
+ // 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;
+ }
+
+ /**
+ * @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;
+ }
+}