Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
}
Expand All @@ -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(
Expand All @@ -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 {
Expand All @@ -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];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
142 changes: 142 additions & 0 deletions packages/contracts-diamond/contracts/assets/ERC721/ERC721DropLib.sol
Original file line number Diff line number Diff line change
@@ -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];
}
}
Loading