This test will show 1 failed since we have not solved the challenge yet. Once we code up our solution in reaper/test/ReaperHack.t.sol, we can run forge test to check the solution.
Code Audit
Overview
Open reaper/src/ReaperVaultV2.sol in Visual Studio. The contract inheritance tells us many things:
This contract is unlikely to be vulnerable to reentrancy attack
Digress: ERC-4626 Rounding Issue
Here we digress a bit from the reaper contract. Regarding ERC-4626, we should know the following things:
What is vault
Why ERC-4626
ERC-4626 Rounding Issue
Vault and ERC-4626
A vault is a multi-sig solution or smart contract that can store and manage assets such as crypto. Each vault always has the tokens it generates as a form of returns. These generated tokens can later be exchanged for tokens that were originally locked in vaults.
The problem developers face concerning yield-bearing tokens is integrating tokens of different protocols. Before ERC-4626, developers have to research a bunch of protocols and figure out how to stitch them together. This can be a pain in the ass and it is also error-prone.
The main benefit of ERC-4626 is that it standardizes tokenized vaults to make protocol integration easier and less prone to error. Since there is a common standard that you can integrate, there is no actual need to build separate adapters any longer. In a nutshell, it quickens development; composability at its peak.
ERC-4626 Rounding Issue
The ERC-4626 rounding issue often appears in Code4rena reports, for example:
deposit() and withdraw() Functions
For ERC20-like contracts, the deposit() and withdraw() functions are likely to be vulnerable since they are directly associated with transactions, hence directly associated with user's money.
deposit()
functiondeposit(uint256 assets,address receiver) publicnonReentrantreturns (uint256 shares) {require(!emergencyShutdown,"Cannot deposit during emergency shutdown");require(assets !=0,"please provide amount");uint256 _pool =totalAssets();require(_pool + assets <= tvlCap,"vault is full!"); shares =previewDeposit(assets);IERC20Metadata(asset).safeTransferFrom(msg.sender,address(this), assets);_mint(receiver, shares);emitDeposit(msg.sender, receiver, assets, shares);}
This function is simple enough:
It checks if the contract is in "emergency shutdown" state
It checks if the assets argument is 0
It checks if the pool is reaching its capacity
It collects token from the user and mints corresponding shares back to the user
The vulnerability is pretty obvious: this function never verifies whether msg.sender is the owner or not. As a result, an attacker can feed his/her own address into this receiver field and specify any account to be the owner. When the withdraw() function is called, all the money in the vicitm account will be transferred to the attacker's account.
Solution
To exploit this vulnerability, we are going to look for some "rich" addresses and steal money from them by calling the withdraw() function.
First, we go to the vault and hunt for some "rich" addresses. Click on "DAI Crypt (rfDAI)":
Grab a few addresses with large "Quantity" field:
Make sure you click into the address and copy the address in "correct" format. The correct address format contains uppercase letters, for example:
Next, we want to figure out how much money they have in these addresses. To do this, we use the reaper.balanceOf() function:
// Enumerate account balanceconsole.log("balance of account 1: ", reaper.balanceOf(0xB573f01f2901c0dB3E14Ec80C6E12e4868DEC864)/1e18);console.log("balance of account 2: ", reaper.balanceOf(0x954773dD09a0bd708D3C03A62FB0947e8078fCf9)/1e18);console.log("balance of account 3: ", reaper.balanceOf(0xfc83DA727034a487f031dA33D55b4664ba312f1D)/1e18);console.log("balance of account 4: ", reaper.balanceOf(0xEB7a12fE169C98748EB20CE8286EAcCF4876643b)/1e18);
Here we divide each balance by 1e18 to convert wei to ether.
Use the following command to compile the contract and see the output of console.log():
forgetest-vv
We will satisfy the 400k DAI requirement if we steal all money from these 4 addresses.
Finally, we are going to steal money from these addresses by calling a series of withdraw() functions: