Steemhunt/mint.club-v2-contract

A user can easily claim all of the distributions when `merkleRoot` is not set

Closed this issue · 2 comments

Severity

Impact: High, as all the tokens can be claimed by a single user

Likelihood: High, as there are no validations checks and a user can create as many accounts as he wants

Description

When createDistribution is called, there are few input parameters that are not validated and are optional as stated by the comments:

    function createDistribution(
        address token,
        bool isERC20,
        uint96 amountPerClaim,
        uint24 walletCount,
        uint40 startTime,
        uint40 endTime,
        bytes32 merkleRoot, // optional
        string calldata title, // optional
        string calldata ipfsCID // optional
    ) external {
        if (token == address(0)) revert MerkleDistributor__InvalidParams('token');
        if (amountPerClaim == 0) revert MerkleDistributor__InvalidParams('amountPerClaim');
        if (walletCount == 0) revert MerkleDistributor__InvalidParams('walletCount');
        if (endTime <= block.timestamp) revert MerkleDistributor__InvalidParams('endTime');
        if (startTime >= endTime) revert MerkleDistributor__InvalidParams('startTime');

       ...
        distribution.merkleRoot = merkleRoot; // optional
        distribution.title = title; // optional
        distribution.ipfsCID = ipfsCID; // optional

        emit Created(distributions.length - 1, token, isERC20, startTime);
    }

This means that the merkleRoot can be set equal to bytes32(0). The problem arises if the merkleRoot is not set because the verification inside claim will be skipped:

    function claim(uint256 distributionId, bytes32[] calldata merkleProof) external {
        Distribution storage distribution = distributions[distributionId];
        ...
        // Verify the merkle proof
        if (distribution.merkleRoot != bytes32(0) && !MerkleProof.verify( //@audit this will be skipped
            merkleProof,
            distribution.merkleRoot,
            keccak256(abi.encodePacked(msg.sender))
        )) revert MerkleDistributor__InvalidProof();

        // Mark it claimed and send the token
        distribution.isClaimed[msg.sender] = true;
        distribution.claimedCount += 1;

        if (distribution.isERC20) {
            IERC20(distribution.token).safeTransfer(msg.sender, distribution.amountPerClaim);
        } else {
            // Only support an ERC1155 token at id = 0
            IERC1155(distribution.token).safeTransferFrom(address(this), msg.sender, 0, distribution.amountPerClaim, "");
        }

        emit Claimed(distributionId, msg.sender);
    }

If distribution.merkleRoot = bytes32(0) the if block will be skipped, then the msg.sender will be marked as claimed and the tokens will be transferred. The problem is that a user can create as many accounts as he wishes and there is absolutely no verification who will claim the distribution as long as it is not the same msg.sender. This can lead to the same user claiming all the tokens from different accounts.

Recommendations

Consider not allowing to create distribution without passing a valid merkleRoot.

  • I also thought about it. But, while creating a distribution, setting the merkleRoot is optional
function createDistribution(
        address token,
        bool isERC20,
        uint96 amountPerClaim,
        uint24 walletCount,
        uint40 startTime,
        uint40 endTime,
        bytes32 merkleRoot, // optional
        string calldata title, // optional
        string calldata ipfsCID // optional
  • In this scenario, the responsibility falls on the creator to set the merkleRoot.

  • To mitigate this, there are a couple of approaches:

    • Implement a Warning: A warning that alerts the creator about the risks associated with not setting a merkleRoot. This can be a comment in the code or front-end.
    • Making merkleRoot Mandatory: A more stringent, yet safer approach would be to make the merkleRoot parameter mandatory. This would prevent the function from executing unless a merkleRoot is specified, thereby ensuring that the token distribution cannot be monopolized by a single user.

Thanks for your suggestion.

Like @0x3agle mentioned, we made the merkleRoot optional intentionally because there's a need for open airdrops (similar use case scenario with BRC20), first-come-first-serve style, even with those multiple alt accounts. They have to pay the gas fees as the minimal acquisition cost anyway.

This is just a tool, so we don't want to restrict user behavior, leaving it as the creator's responsibility. I added a notice comment on the function call and will also add a warning on the front-end.