diff --git a/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimLib.sol b/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimLib.sol index 05b0927e..b47324e8 100644 --- a/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimLib.sol +++ b/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimLib.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AccessControlRecursiveLib} from "../../access/AccessControlRecursiveLib.sol"; -import {IERC721ClaimMultiPhase} from "./IERC721ClaimMultiPhase.sol"; +import {IERC721ClaimSinglePhase} from "./IERC721ClaimSinglePhase.sol"; library ERC721ClaimLib { - bytes32 internal constant ERC721_CLAIM_ROLE = bytes32(IERC721ClaimMultiPhase.setClaimCondition.selector); + bytes32 internal constant ERC721_CLAIM_ROLE = bytes32(IERC721ClaimSinglePhase.setClaimCondition.selector); bytes32 constant ERC721_CLAIM_STORAGE = keccak256(abi.encode(uint256(keccak256("erc721.claim.storage")) - 1)) & ~bytes32(uint256(0xff)); @@ -16,15 +16,19 @@ library ERC721ClaimLib { uint256 startTimestamp; uint256 endTimestamp; uint256 maxClaimableSupply; - uint256 supplyClaimed; uint256 quantityLimitPerWallet; uint256 pricePerToken; address currency; } + struct ClaimConditionState { + uint256 supplyClaimed; + mapping(address => uint256) accountClaims; + } + struct ERC721ClaimStorage { mapping(bytes32 => ClaimCondition) claimConditions; - mapping(bytes32 => mapping(address => uint256)) walletClaims; + mapping(bytes32 => ClaimConditionState) claimConditionStates; } // Errors @@ -52,11 +56,6 @@ library ERC721ClaimLib { revert InvalidClaimCondition("maxClaimableSupply cannot be zero."); } - ClaimCondition storage existingCondition = ds.claimConditions[conditionId]; - - // condition.supplyClaimed cannot be edited - newCondition.supplyClaimed = existingCondition.supplyClaimed; - ds.claimConditions[conditionId] = newCondition; emit ClaimConditionSet(conditionId, newCondition); } @@ -66,24 +65,25 @@ library ERC721ClaimLib { return ds.claimConditions[conditionId]; } - function _getWalletClaims(bytes32 conditionId, address account) internal view returns (uint256) { + function _getAccountClaims(bytes32 conditionId, address account) internal view returns (uint256) { ERC721ClaimStorage storage ds = getData(); - return ds.walletClaims[conditionId][account]; + return ds.claimConditionStates[conditionId].accountClaims[account]; } - function _conditionExists(bytes32 conditionId, ERC721ClaimStorage storage ds) internal view returns (bool) { - return ds.claimConditions[conditionId].maxClaimableSupply != 0; - } - - function _checkClaim(ClaimCondition memory condition, uint256 quantity, uint256 accountClaimed) internal view { + function _checkClaim( + ClaimCondition memory condition, + ClaimConditionState storage state, + uint256 quantity, + address account + ) internal view { if (!_isWithinClaimPeriod(condition)) { revert ClaimPeriodNotActive(block.timestamp, condition.startTimestamp, condition.endTimestamp); } - if (!_hasSufficientSupply(condition, quantity)) { - revert ExceedsMaxClaimableSupply(condition.supplyClaimed + quantity, condition.maxClaimableSupply); + if (!_hasSufficientSupply(state, condition, quantity)) { + revert ExceedsMaxClaimableSupply(state.supplyClaimed + quantity, condition.maxClaimableSupply); } - if (!_isWithinUserLimit(condition, quantity, accountClaimed)) { - uint256 remainingLimit = condition.quantityLimitPerWallet - accountClaimed; + if (!_isWithinUserLimit(condition, quantity, state.accountClaims[account])) { + uint256 remainingLimit = condition.quantityLimitPerWallet - state.accountClaims[account]; revert ExceedsWalletLimit(quantity, remainingLimit); } } @@ -92,8 +92,12 @@ library ERC721ClaimLib { return block.timestamp >= condition.startTimestamp && block.timestamp <= condition.endTimestamp; } - function _hasSufficientSupply(ClaimCondition memory condition, uint256 quantity) internal pure returns (bool) { - return condition.supplyClaimed + quantity <= condition.maxClaimableSupply; + function _hasSufficientSupply( + ClaimConditionState storage state, + ClaimCondition memory condition, + uint256 quantity + ) internal view returns (bool) { + return state.supplyClaimed + quantity <= condition.maxClaimableSupply; } function _isWithinUserLimit( @@ -104,14 +108,9 @@ library ERC721ClaimLib { return userClaimed + quantity <= condition.quantityLimitPerWallet; } - function _updateClaim( - ClaimCondition storage condition, - mapping(address => uint256) storage conditionWalletClaims, - address to, - uint256 quantity - ) internal { - conditionWalletClaims[to] += quantity; - condition.supplyClaimed += quantity; + function _updateClaim(ClaimConditionState storage state, address account, uint256 quantity) internal { + state.accountClaims[account] += quantity; + state.supplyClaimed += quantity; } function _payClaim(ClaimCondition memory condition, address account, uint256 quantity) internal { @@ -132,13 +131,14 @@ library ERC721ClaimLib { function _claim(bytes32 conditionId, address account, uint256 quantity) internal returns (uint256) { ERC721ClaimStorage storage ds = getData(); - ClaimCondition storage condition = ds.claimConditions[conditionId]; + ClaimCondition memory condition = ds.claimConditions[conditionId]; + ClaimConditionState storage state = ds.claimConditionStates[conditionId]; - _checkClaim(condition, quantity, ds.walletClaims[conditionId][account]); - _updateClaim(condition, ds.walletClaims[conditionId], account, quantity); + _checkClaim(condition, state, quantity, account); + _updateClaim(state, account, quantity); _payClaim(condition, account, quantity); emit TokensClaimed(account, conditionId, quantity); - return ds.walletClaims[conditionId][account]; + return state.accountClaims[account]; } } diff --git a/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimSinglePhaseFacet.sol b/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimSinglePhaseFacet.sol index b5e340b2..92bf5d55 100644 --- a/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimSinglePhaseFacet.sol +++ b/packages/contracts-diamond/contracts/assets/ERC721/ERC721ClaimSinglePhaseFacet.sol @@ -94,7 +94,7 @@ contract ERC721ClaimSinglePhaseFacet is IERC721ClaimSinglePhase { * @notice Get the number of claims for a given wallet address. * @param account The wallet address to check. */ - function getWalletClaims(address account) external view returns (uint256) { - return ERC721ClaimLib._getWalletClaims(0, account); + function getAccountClaims(address account) external view returns (uint256) { + return ERC721ClaimLib._getAccountClaims(0, account); } } diff --git a/packages/contracts-diamond/contracts/assets/ERC721/ERC721DropLib.sol b/packages/contracts-diamond/contracts/assets/ERC721/ERC721DropLib.sol new file mode 100644 index 00000000..f3a76289 --- /dev/null +++ b/packages/contracts-diamond/contracts/assets/ERC721/ERC721DropLib.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {AccessControlRecursiveLib} from "../../access/AccessControlRecursiveLib.sol"; +import {IERC721DropSinglePhase} from "./IERC721DropSinglePhase.sol"; +import {ERC721ClaimLib} from "./ERC721ClaimLib.sol"; + +library ERC721DropLib { + using ERC721ClaimLib for ERC721ClaimLib.ClaimCondition; + + bytes32 internal constant ERC721_DROP_ROLE = bytes32(IERC721DropSinglePhase.setDropCondition.selector); + + bytes32 constant ERC721_DROP_STORAGE = + keccak256(abi.encode(uint256(keccak256("erc721.drop.storage")) - 1)) & ~bytes32(uint256(0xff)); + + /// @custom:storage-location erc7201:erc721.drop.storage + struct DropCondition { + bytes32 merkleRoot; + } + + struct DropConditionState { + mapping(address => uint256) accountClaims; + } + + struct DropStorage { + mapping(bytes32 => DropCondition) dropConditions; + mapping(bytes32 => DropConditionState) dropConditionStates; + } + + // Errors + error InvalidProof(); + error DropConditionNotFound(bytes32 dropConditionId); + + // Events + event DropConditionSet(bytes32 indexed dropConditionId, bytes32 merkleRoot); + event TokensClaimedWithProof( + bytes32 indexed dropConditionId, + address indexed account, + uint256 quantity, + uint256 totalClaimed + ); + + function getData() internal pure returns (DropStorage storage ds) { + bytes32 position = ERC721_DROP_STORAGE; + assembly { + ds.slot := position + } + } + + /** + * @dev Set or update a drop condition. + * Requires the caller to have the `ERC721_DROP_ROLE`. + * + * @param dropConditionId Unique ID for the drop condition. + * @param root Merkle root for address-based claim limits. + */ + function _setDropCondition(bytes32 dropConditionId, bytes32 root) internal { + AccessControlRecursiveLib._checkRoleRecursive(ERC721_DROP_ROLE, msg.sender); + + DropStorage storage ds = getData(); + DropCondition storage condition = ds.dropConditions[dropConditionId]; + condition.merkleRoot = root; + + emit DropConditionSet(dropConditionId, root); + } + + /** + * @dev Verify Merkle proof and update account claims for a specific drop condition. + * + * @param dropConditionId The ID of the drop condition. + * @param account Address of the account making the claim. + * @param quantity Number of tokens the account wants to claim. + * @param condition Claim condition struct containing all condition fields. + * @param merkleProof Array of hashes required to verify the account's eligibility. + */ + function _claimWithProof( + bytes32 dropConditionId, + address account, + uint256 quantity, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) internal { + DropStorage storage ds = getData(); + DropCondition storage drop = ds.dropConditions[dropConditionId]; + DropConditionState storage state = ds.dropConditionStates[dropConditionId]; + + if (drop.merkleRoot == bytes32(0)) { + revert DropConditionNotFound(dropConditionId); + } + + if (!_verifyAccountClaimCondition(drop.merkleRoot, account, condition, merkleProof)) { + revert InvalidProof(); + } + + condition._checkClaim(state, quantity, account); + condition._payClaim(account, quantity); + state.accountClaims[account] += quantity; + + emit TokensClaimedWithProof(dropConditionId, account, quantity, state.accountClaims[account]); + } + + /** + * @dev Verify the account's conditions using the provided Merkle proof. + * @param merkleRoot Merkle root for the condition. + * @param account Address of the account to check. + * @param condition ClaimCondition struct to validate. + * @param proof Array of hashes required to verify the account's eligibility. + * @return boolean indicating if the proof is valid. + */ + function _verifyAccountClaimCondition( + bytes32 merkleRoot, + address account, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata proof + ) internal pure returns (bool) { + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, condition)))); + + return MerkleProof.verify(proof, merkleRoot, leaf); + } + + /** + * @notice Retrieve the Merkle root of a specific drop condition. + * @param dropConditionId The ID of the drop condition. + * @return bytes32 Merkle root of the drop condition. + */ + function _getDropConditionMerkleRoot(bytes32 dropConditionId) internal view returns (bytes32) { + DropStorage storage ds = getData(); + return ds.dropConditions[dropConditionId].merkleRoot; + } + + /** + * @dev Get the number of tokens already claimed by an account for a specific drop condition. + * @param dropConditionId The ID of the drop condition. + * @param account Address of the account. + * @return uint256 Number of tokens claimed. + */ + function _getDropConditionAccountClaimed(bytes32 dropConditionId, address account) internal view returns (uint256) { + DropStorage storage ds = getData(); + return ds.dropConditionStates[dropConditionId].accountClaims[account]; + } +} diff --git a/packages/contracts-diamond/contracts/assets/ERC721/ERC721DropSinglePhaseFacet.sol b/packages/contracts-diamond/contracts/assets/ERC721/ERC721DropSinglePhaseFacet.sol new file mode 100644 index 00000000..17c81816 --- /dev/null +++ b/packages/contracts-diamond/contracts/assets/ERC721/ERC721DropSinglePhaseFacet.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControlRecursiveLib} from "../../access/AccessControlRecursiveLib.sol"; + +import {IERC721DropSinglePhase} from "./IERC721DropSinglePhase.sol"; +import {IERC721MintableAutoId} from "./IERC721MintableAutoId.sol"; + +import {ERC721DropLib} from "./ERC721DropLib.sol"; +import {ERC721MintableAutoIdLib} from "./ERC721MintableAutoIdLib.sol"; +import {ERC721ClaimLib} from "./ERC721ClaimLib.sol"; + +/** + * @dev Manage and claim ERC-721 tokens airdrops in a single phase. + */ +contract ERC721DropSinglePhaseFacet is IERC721DropSinglePhase { + using ERC721ClaimLib for ERC721ClaimLib.ClaimCondition; + + /** + * @notice Set or update the drop condition. + * Caller must have the `ERC721_DROP_ROLE`. + * @param merkleRoot Merkle root for the address-based claim limits. + */ + function setDropCondition(bytes32 merkleRoot) external { + ERC721DropLib._setDropCondition(0, merkleRoot); + } + + /** + * @notice Mint a single token to the specified address. + * @param to The address to mint the token to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Array of hashes required to verify the account's eligibility. + */ + function mintWithProof( + address to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256) { + if (AccessControlRecursiveLib._hasRoleRecursive(ERC721MintableAutoIdLib.ERC721_MINTER_ROLE, msg.sender)) { + return ERC721MintableAutoIdLib.__unsafe_mint(to); + } else { + ERC721DropLib._claimWithProof(0, msg.sender, 1, condition, merkleProof); + return ERC721MintableAutoIdLib.__unsafe_mint(to); + } + } + + /** + * @notice Mint multiple tokens in a batch. + * @param to The addresses to mint the tokens to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Array of hashes required to verify the account's eligibility. + */ + function mintBatchWithProof( + address[] memory to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256[] memory) { + if (AccessControlRecursiveLib._hasRoleRecursive(ERC721MintableAutoIdLib.ERC721_MINTER_ROLE, msg.sender)) { + return ERC721MintableAutoIdLib.__unsafe_mintBatch(to); + } else { + ERC721DropLib._claimWithProof(0, msg.sender, to.length, condition, merkleProof); + return ERC721MintableAutoIdLib.__unsafe_mintBatch(to); + } + } + + /** + * @notice Safely mint a single token to the specified address. + * @param to The address to mint the token to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Array of hashes required to verify the account's eligibility. + */ + function safeMintWithProof( + address to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256) { + if (AccessControlRecursiveLib._hasRoleRecursive(ERC721MintableAutoIdLib.ERC721_MINTER_ROLE, msg.sender)) { + return ERC721MintableAutoIdLib.__unsafe_safeMint(to); + } else { + ERC721DropLib._claimWithProof(0, msg.sender, 1, condition, merkleProof); + return ERC721MintableAutoIdLib.__unsafe_safeMint(to); + } + } + + /** + * @notice Safely mint multiple tokens in a batch. + * @param to The addresses to mint the tokens to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Array of hashes required to verify the account's eligibility. + */ + function safeMintBatchWithProof( + address[] memory to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256[] memory) { + if (AccessControlRecursiveLib._hasRoleRecursive(ERC721MintableAutoIdLib.ERC721_MINTER_ROLE, msg.sender)) { + return ERC721MintableAutoIdLib.__unsafe_safeMintBatch(to); + } else { + ERC721DropLib._claimWithProof(0, msg.sender, to.length, condition, merkleProof); + return ERC721MintableAutoIdLib.__unsafe_safeMintBatch(to); + } + } + + /** + * @notice Get the number of tokens already claimed by an account. + * @param account Address of the account. + * @return uint256 Number of tokens claimed. + */ + function getAccountClaimed(address account) external view returns (uint256) { + return ERC721DropLib._getDropConditionAccountClaimed(0, account); + } + + /** + * @notice Get the Merkle root. + * @return bytes32 The Merkle root. + */ + function getMerkleRoot() external view returns (bytes32) { + return ERC721DropLib._getDropConditionMerkleRoot(0); + } +} diff --git a/packages/contracts-diamond/contracts/assets/ERC721/IERC721ClaimSinglePhase.sol b/packages/contracts-diamond/contracts/assets/ERC721/IERC721ClaimSinglePhase.sol index 5dd0cbeb..ed034670 100644 --- a/packages/contracts-diamond/contracts/assets/ERC721/IERC721ClaimSinglePhase.sol +++ b/packages/contracts-diamond/contracts/assets/ERC721/IERC721ClaimSinglePhase.sol @@ -35,5 +35,5 @@ interface IERC721ClaimSinglePhase { * @param account The address of the wallet to check. * @return uint256 The number of claims made by the account. */ - function getWalletClaims(address account) external view returns (uint256); + function getAccountClaims(address account) external view returns (uint256); } diff --git a/packages/contracts-diamond/contracts/assets/ERC721/IERC721DropSinglePhase.sol b/packages/contracts-diamond/contracts/assets/ERC721/IERC721DropSinglePhase.sol new file mode 100644 index 00000000..9ec2ca34 --- /dev/null +++ b/packages/contracts-diamond/contracts/assets/ERC721/IERC721DropSinglePhase.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC721ClaimLib} from "./ERC721ClaimLib.sol"; + +interface IERC721DropSinglePhase { + event DropConditionSet(bytes32 indexed dropConditionId, bytes32 merkleRoot); + event TokensClaimedWithProof( + bytes32 indexed dropConditionId, + address indexed account, + uint256 quantity, + uint256 totalClaimed + ); + + /** + * @notice Set or update the drop condition. + * Caller must have the appropriate role. + * @param merkleRoot Merkle root for the address-based claim limits. + */ + function setDropCondition(bytes32 merkleRoot) external; + + /** + * @notice Mint a single token with proof. + * @param to Address to mint the token to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Merkle proof array. + */ + function mintWithProof( + address to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256); + + /** + * @notice Mint multiple tokens with proof. + * @param to Array of addresses to mint tokens to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Merkle proof array. + */ + function mintBatchWithProof( + address[] memory to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256[] memory); + + /** + * @notice Safely mint a single token with proof. + * @param to Address to mint the token to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Merkle proof array. + */ + function safeMintWithProof( + address to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256); + + /** + * @notice Safely mint multiple tokens with proof. + * @param to Array of addresses to mint tokens to. + * @param condition ClaimCondition struct containing all condition fields. + * @param merkleProof Merkle proof array. + */ + function safeMintBatchWithProof( + address[] memory to, + ERC721ClaimLib.ClaimCondition calldata condition, + bytes32[] calldata merkleProof + ) external payable returns (uint256[] memory); + + /** + * @notice Get the number of tokens already claimed by an account. + * @param account Address of the account. + * @return uint256 Number of tokens claimed. + */ + function getAccountClaimed(address account) external view returns (uint256); + + /** + * @notice Get the Merkle root. + * @return bytes32 The Merkle root. + */ + function getMerkleRoot() external view returns (bytes32); +} diff --git a/packages/contracts-diamond/contracts/assets/ERC721/presets/ERC721DropSinglePhase.sol b/packages/contracts-diamond/contracts/assets/ERC721/presets/ERC721DropSinglePhase.sol new file mode 100644 index 00000000..889c8291 --- /dev/null +++ b/packages/contracts-diamond/contracts/assets/ERC721/presets/ERC721DropSinglePhase.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC165Facet} from "../../../ERC165/ERC165Facet.sol"; +import {ERC165Lib} from "../../../ERC165/ERC165Lib.sol"; + +import {IAccessControl} from "../../../access/IAccessControl.sol"; +import {IAccessControlRecursive} from "../../../access/IAccessControlRecursive.sol"; +import {AccessControlRecursiveFacet} from "../../../access/AccessControlRecursiveFacet.sol"; + +import {AccessControlLib} from "../../../access/AccessControlLib.sol"; + +import {IContractURI} from "../../../ContractURI/IContractURI.sol"; +import {ContractURILib} from "../../../ContractURI/ContractURILib.sol"; +import {ContractURIFacet} from "../../../ContractURI/ContractURIFacet.sol"; + +import {IERC721} from "../IERC721.sol"; +import {ERC721Lib} from "../ERC721Lib.sol"; + +import {IERC721MintableAutoId} from "../IERC721MintableAutoId.sol"; +import {IERC721DropSinglePhase} from "../IERC721DropSinglePhase.sol"; +import {ERC721DropSinglePhaseFacet} from "../ERC721DropSinglePhaseFacet.sol"; +import {ERC721DropLib} from "../ERC721DropLib.sol"; + +import {IERC721Metadata} from "../IERC721Metadata.sol"; +import {IERC721BaseURI} from "../IERC721BaseURI.sol"; +import {ERC721BaseURILib} from "../ERC721BaseURILib.sol"; +import {ERC721BaseURIFacet} from "../ERC721BaseURIFacet.sol"; + +import {IERC2981} from "../../../ERC2981/IERC2981.sol"; +import {ERC2981Lib} from "../../../ERC2981/ERC2981Lib.sol"; +import {ERC2981Facet} from "../../../ERC2981/ERC2981Facet.sol"; + +contract ERC721DropPreset is ERC165Facet, AccessControlRecursiveFacet, ERC721DropSinglePhaseFacet, ERC721BaseURIFacet { + constructor( + address admin, + string memory contractUri, + string memory name, + string memory symbol, + string memory baseUri, + address royaltyReceiver, + uint96 feeNumerator, + bytes32 merkleRoot + ) { + AccessControlLib.__unsafe_grantRole(AccessControlLib.DEFAULT_ADMIN_ROLE, admin); + + ERC165Lib._init(); + ERC165Lib.__unsafe_registerInterface(type(IAccessControl).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IAccessControlRecursive).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IContractURI).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IERC721).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IERC721MintableAutoId).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IERC721DropSinglePhase).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IERC721Metadata).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IERC721BaseURI).interfaceId, true); + ERC165Lib.__unsafe_registerInterface(type(IERC2981).interfaceId, true); + + ContractURILib._init(contractUri); + ERC721Lib._init(name, symbol); + ERC721BaseURILib._init(baseUri); + ERC2981Lib._init(royaltyReceiver, feeNumerator); + + ERC721DropLib._setDropCondition(0, merkleRoot); + } +} diff --git a/packages/contracts-diamond/package.json b/packages/contracts-diamond/package.json index 1380d8ff..16d76e29 100644 --- a/packages/contracts-diamond/package.json +++ b/packages/contracts-diamond/package.json @@ -91,6 +91,7 @@ }, "dependencies": { "@openzeppelin/contracts": "5.0.2", + "@openzeppelin/merkle-tree": "^1.0.7", "@owlprotocol/contracts-create2factory": "workspace:*", "@owlprotocol/viem-utils": "workspace:*", "@owlprotocol/zod-sol": "workspace:*", diff --git a/packages/contracts-diamond/src/ERC721DropSinglePhase.test.ts b/packages/contracts-diamond/src/ERC721DropSinglePhase.test.ts new file mode 100644 index 00000000..07042a6c --- /dev/null +++ b/packages/contracts-diamond/src/ERC721DropSinglePhase.test.ts @@ -0,0 +1,212 @@ +import { describe, test, beforeAll, beforeEach, expect } from "vitest"; +import { + Account, + Address, + Chain, + PublicClient, + Transport, + WalletClient, + createPublicClient, + createWalletClient, + http, + zeroAddress, + parseEther, + hexToBigInt, + Hex, +} from "viem"; +import { localhost } from "viem/chains"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; +import { getLocalAccount } from "@owlprotocol/viem-utils"; +import { ERC721DropPreset } from "./artifacts/ERC721DropPreset.js"; +import { port } from "./test/constants.js"; + +const MAX_INT = hexToBigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + +const ClaimConditionAbi = { + components: [ + { internalType: "uint256", name: "startTimestamp", type: "uint256" }, + { internalType: "uint256", name: "endTimestamp", type: "uint256" }, + { internalType: "uint256", name: "maxClaimableSupply", type: "uint256" }, + { internalType: "uint256", name: "supplyClaimed", type: "uint256" }, + { internalType: "uint256", name: "quantityLimitPerWallet", type: "uint256" }, + { internalType: "uint256", name: "pricePerToken", type: "uint256" }, + { internalType: "address", name: "currency", type: "address" }, + ], + internalType: "struct ERC721ClaimLib.ClaimCondition", + name: "condition", + type: "tuple", +}; + +describe("ERC721DropPreset with Single Valid and Invalid User", () => { + let transport: Transport; + let publicClient: PublicClient; + let adminWalletClient: WalletClient; + let contractAddress: Address; + + const userWalletClients: WalletClient[] = []; + let tree: StandardMerkleTree; + + let validAddress: Address; + let invalidAddress: Address; + + const claimCondition = { + startTimestamp: 0n, + endTimestamp: MAX_INT, + maxClaimableSupply: 3n, + supplyClaimed: 0n, + quantityLimitPerWallet: 3n, + pricePerToken: parseEther("0.1"), + currency: zeroAddress, + }; + + beforeAll(async () => { + transport = http(`http://127.0.0.1:${port}`); + publicClient = createPublicClient({ + chain: localhost, + transport, + }); + adminWalletClient = createWalletClient({ + account: getLocalAccount(0), + chain: localhost, + transport, + }); + }); + + beforeEach(async () => { + userWalletClients.length = 0; + + for (let i = 0; i < 2; i++) { + const userWalletClient = createWalletClient({ + account: privateKeyToAccount(generatePrivateKey()), + chain: localhost, + transport, + }); + userWalletClients.push(userWalletClient); + + const fundHash = await adminWalletClient.sendTransaction({ + to: userWalletClient.account.address, + value: parseEther("1"), + }); + await publicClient.waitForTransactionReceipt({ hash: fundHash }); + } + + validAddress = userWalletClients[0].account.address; + invalidAddress = userWalletClients[1].account.address; + + const valuesAbi = [{ type: "address" }, ClaimConditionAbi] as any; + const values = [ + [ + validAddress, + [ + claimCondition.startTimestamp, + claimCondition.endTimestamp, + claimCondition.maxClaimableSupply, + claimCondition.supplyClaimed, + claimCondition.quantityLimitPerWallet, + claimCondition.pricePerToken, + claimCondition.currency, + ], + ], + ]; + + tree = StandardMerkleTree.of(values, valuesAbi); + + const hashDeploy = await adminWalletClient.deployContract({ + abi: ERC721DropPreset.abi, + args: [ + adminWalletClient.account.address, + "contractUri", + "Token", + "MYT", + "baseUri", + adminWalletClient.account.address, + 500n, + tree.root as Hex, + ], + bytecode: ERC721DropPreset.bytecode, + }); + + const receiptDeploy = await publicClient.waitForTransactionReceipt({ hash: hashDeploy }); + contractAddress = receiptDeploy.contractAddress!; + }); + + test("Mint and batch mint with the valid user in the Merkle Tree", async () => { + const proof = tree.getProof(0) as Hex[]; + + const { request } = await publicClient.simulateContract({ + account: userWalletClients[0].account, + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "mintWithProof", + args: [validAddress, claimCondition, proof], + value: claimCondition.pricePerToken, + }); + + const hash = await userWalletClients[0].writeContract(request); + await publicClient.waitForTransactionReceipt({ hash }); + + const ownerOf = await publicClient.readContract({ + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "ownerOf", + args: [1n], + }); + + expect(ownerOf).toBe(validAddress); + + const { request: batchRequest } = await publicClient.simulateContract({ + account: userWalletClients[0].account, + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "mintBatchWithProof", + args: [[validAddress], claimCondition, proof], + value: claimCondition.pricePerToken, + }); + + const batchHash = await userWalletClients[0].writeContract(batchRequest); + await publicClient.waitForTransactionReceipt({ hash: batchHash }); + + const ownerOfBatch = await publicClient.readContract({ + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "ownerOf", + args: [2n], + }); + + expect(ownerOfBatch).toBe(validAddress); + + const claimed = await publicClient.readContract({ + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "getAccountClaimed", + args: [validAddress], + }); + expect(claimed).toBe(2n); + }); + + test("Mint with the invalid user not in the Merkle Tree should fail", async () => { + const emptyProof: Address[] = []; + + await expect( + publicClient.simulateContract({ + account: userWalletClients[1].account, + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "mintWithProof", + args: [invalidAddress, claimCondition, emptyProof], + value: claimCondition.pricePerToken, + }), + ).rejects.toThrowError("InvalidProof"); + }); + + test("Verify getMerkleRoot", async () => { + const merkleRoot = await publicClient.readContract({ + address: contractAddress, + abi: ERC721DropPreset.abi, + functionName: "getMerkleRoot", + }); + + expect(merkleRoot).toBe(tree.root); + }); +}); diff --git a/packages/contracts-diamond/src/ERC721SinglePhase.test.ts b/packages/contracts-diamond/src/ERC721SinglePhase.test.ts index d6d7b764..ca49e838 100644 --- a/packages/contracts-diamond/src/ERC721SinglePhase.test.ts +++ b/packages/contracts-diamond/src/ERC721SinglePhase.test.ts @@ -19,90 +19,6 @@ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; import { ERC721SinglePhasePreset } from "./artifacts/ERC721SinglePhasePreset.js"; import { port } from "./test/constants.js"; -const constructorAbi = { - inputs: [ - { - internalType: "address", - name: "admin", - type: "address", - }, - { - internalType: "string", - name: "contractUri", - type: "string", - }, - { - internalType: "string", - name: "name", - type: "string", - }, - { - internalType: "string", - name: "symbol", - type: "string", - }, - { - internalType: "string", - name: "baseUri", - type: "string", - }, - { - internalType: "address", - name: "royaltyReceiver", - type: "address", - }, - { - internalType: "uint96", - name: "feeNumerator", - type: "uint96", - }, - { - components: [ - { - internalType: "uint256", - name: "startTimestamp", - type: "uint256", - }, - { - internalType: "uint256", - name: "endTimestamp", - type: "uint256", - }, - { - internalType: "uint256", - name: "maxClaimableSupply", - type: "uint256", - }, - { - internalType: "uint256", - name: "supplyClaimed", - type: "uint256", - }, - { - internalType: "uint256", - name: "quantityLimitPerWallet", - type: "uint256", - }, - { - internalType: "uint256", - name: "pricePerToken", - type: "uint256", - }, - { - internalType: "address", - name: "currency", - type: "address", - }, - ], - internalType: "struct ERC721ClaimLib.ClaimCondition", - name: "condition", - type: "tuple", - }, - ], - stateMutability: "nonpayable", - type: "constructor", -}; - const MAX_INT = hexToBigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); describe("ERC721SinglePhase.test.ts", function () { @@ -157,7 +73,7 @@ describe("ERC721SinglePhase.test.ts", function () { }; const hashDeploy = await adminWalletClient.deployContract({ - abi: [constructorAbi], + abi: ERC721SinglePhasePreset.abi, args: [ adminWalletClient.account.address, "contractUri", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 727a1099..39eebef7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1085,6 +1085,9 @@ importers: '@openzeppelin/contracts': specifier: 5.0.2 version: 5.0.2 + '@openzeppelin/merkle-tree': + specifier: ^1.0.7 + version: 1.0.7 '@owlprotocol/contracts-create2factory': specifier: workspace:* version: link:../contracts-create2factory @@ -4072,6 +4075,9 @@ packages: '@openzeppelin/contracts@5.0.2': resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} + '@openzeppelin/merkle-tree@1.0.7': + resolution: {integrity: sha512-i93t0YYv6ZxTCYU3CdO5Q+DXK0JH10A4dCBOMlzYbX+ujTXm+k1lXiEyVqmf94t3sqmv8sm/XT5zTa0+efnPgQ==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -7009,6 +7015,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -15295,6 +15302,13 @@ snapshots: '@openzeppelin/contracts@5.0.2': {} + '@openzeppelin/merkle-tree@1.0.7': + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -18504,8 +18518,8 @@ snapshots: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) eslint-config-xo-space: 0.35.0(eslint@8.57.0) - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-mocha: 10.5.0(eslint@8.57.0) eslint-plugin-n: 15.7.0(eslint@8.57.0) eslint-plugin-perfectionist: 2.11.0(eslint@8.57.0)(typescript@5.4.5) @@ -18564,33 +18578,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.5(supports-color@8.1.1) enhanced-resolve: 5.17.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18605,13 +18619,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -18654,7 +18668,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -18665,7 +18679,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.30.0)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3