Abracadabra Mimswap - HChang26's results

General Information

Platform: Code4rena

Start Date: 07/03/2024

Pot Size: $63,000 USDC

Total HM: 20

Participants: 36

Period: 5 days

Judge: cccz

Total Solo HM: 11

Id: 349

League: BLAST

Abracadabra Money

Findings Distribution

Researcher Performance

Rank: 31/36

Findings: 1

Award: $15.33

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

15.328 USDC - $15.33

Labels

bug
downgraded by judge
grade-b
QA (Quality Assurance)
Q-03

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/MagicLP.sol#L413 https://github.com/code-423n4/2024-03-abracadabra-money/blob/main/src/mimswap/MagicLP.sol#L360

Vulnerability details

Impact

Inconsistent rounding in _BASE_TARGET_ and _QUOTE_TARGET_ will deviate away from true targets and price curve.

Proof of Concept

The variables _BASE_TARGET_ and _QUOTE_TARGET_ are crucial for determining the regression target, which in turn affects the calculation of the price curve. These values are updated whenever users execute the buyShares() and sellShares() functions.

In the buyShares() function, the variables baseInputRatio, quoteInputRatio, and shares are rounded down. Additionally, DecimalMath.mulFloor(uint256(_BASE_TARGET_), mintRatio)) and DecimalMath.mulFloor(uint256(_QUOTE_TARGET_), mintRatio)) are also rounded down. This leads to _BASE_TARGET_ and _QUOTE_TARGET_ values being slightly lower than the true targets each time buyShares() is called.

          ->uint256 baseInputRatio = DecimalMath.divFloor(baseInput, baseReserve);
          ->uint256 quoteInputRatio = DecimalMath.divFloor(quoteInput, quoteReserve);
            uint256 mintRatio = quoteInputRatio < baseInputRatio ? quoteInputRatio : baseInputRatio;
          ->shares = DecimalMath.mulFloor(totalSupply(), mintRatio);

          ->_BASE_TARGET_ = (uint256(_BASE_TARGET_) + DecimalMath.mulFloor(uint256(_BASE_TARGET_), mintRatio)).toUint112();
          ->_QUOTE_TARGET_ = (uint256(_QUOTE_TARGET_) + DecimalMath.mulFloor(uint256(_QUOTE_TARGET_), mintRatio)).toUint112();

On the other hand, in the sellShares() function, (uint256(_BASE_TARGET_) * shareAmount).divCeil(totalShares)) and (uint256(_QUOTE_TARGET_) * shareAmount).divCeil(totalShares)) are rounded up. Consequently, _BASE_TARGET_ and _QUOTE_TARGET_ stored in the function are slightly lower than the true targets.

        _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) - (uint256(_BASE_TARGET_) * shareAmount).divCeil(totalShares));
        _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) - (uint256(_QUOTE_TARGET_) * shareAmount).divCeil(totalShares));

As a result of the combined effects of buyShares() and sellShares(), _BASE_TARGET_ and _QUOTE_TARGET_ gradually decrease, deviating from their true targets with each user interaction.

Tools Used

Manual Review

sellShares() should round down.

    function sellShares(
        uint256 shareAmount,
        address to,
        uint256 baseMinAmount,
        uint256 quoteMinAmount,
        bytes calldata data,
        uint256 deadline
    ) external nonReentrant returns (uint256 baseAmount, uint256 quoteAmount) {
        if (deadline < block.timestamp) {
            revert ErrExpired();
        }
        if (shareAmount > balanceOf(msg.sender)) {
            revert ErrNotEnough();
        }
        if (to == address(this)) {
            revert ErrSellBackNotAllowed();
        }

        uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
        uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));
        uint256 totalShares = totalSupply();

        baseAmount = (baseBalance * shareAmount) / totalShares;
        quoteAmount = (quoteBalance * shareAmount) / totalShares;

-       _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) - (uint256(_BASE_TARGET_) * shareAmount).divCeil(totalShares));
+       _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) - (uint256(_BASE_TARGET_) * shareAmount).divFloor(totalShares));
-       _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) - (uint256(_QUOTE_TARGET_) * shareAmount).divCeil(totalShares));
+       _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) - (uint256(_QUOTE_TARGET_) * shareAmount).divFloor(totalShares));

        if (baseAmount < baseMinAmount || quoteAmount < quoteMinAmount) {
            revert ErrWithdrawNotEnough();
        }

        _burn(msg.sender, shareAmount);
        _transferBaseOut(to, baseAmount);
        _transferQuoteOut(to, quoteAmount);
        _sync();

        if (data.length > 0) {
            ICallee(to).SellShareCall(msg.sender, shareAmount, baseAmount, quoteAmount, data);
        }

        emit SellShares(msg.sender, to, shareAmount, balanceOf(msg.sender));
    }

Assessed type

Other

#0 - 0xpoolboy

2024-03-14T19:38:03Z

Bug leads to an off-by-1 error per sellShares()-call at most (minimal impact). => LOW

Correct fix is:

- _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) - (uint256(_BASE_TARGET_) * shareAmount).divCeil(totalShares)); + _BASE_TARGET_ = uint112(uint256(_BASE_TARGET_) - (uint256(_BASE_TARGET_) * shareAmount) / (totalShares)); - _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) - (uint256(_QUOTE_TARGET_) * shareAmount).divCeil(totalShares)); + _QUOTE_TARGET_ = uint112(uint256(_QUOTE_TARGET_) - (uint256(_QUOTE_TARGET_) * shareAmount) / (totalShares));

#1 - c4-pre-sort

2024-03-15T14:39:16Z

141345 marked the issue as duplicate of #221

#2 - c4-judge

2024-03-29T05:36:49Z

thereksfour marked the issue as not a duplicate

#3 - c4-judge

2024-03-29T05:36:56Z

thereksfour changed the severity to QA (Quality Assurance)

#4 - c4-judge

2024-03-29T05:37:20Z

This previously downgraded issue has been upgraded by thereksfour

#5 - c4-judge

2024-03-29T08:39:02Z

thereksfour changed the severity to QA (Quality Assurance)

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter