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
Rank: 31/36
Findings: 1
Award: $15.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ether_sky
Also found by: 0x11singh99, 0xE1, 0xJaeger, Bauchibred, Bigsam, Bozho, Breeje, DarkTower, HChang26, SpicyMeatball, Trust, ZanyBonzy, albahaca, bareli, blutorque, grearlake, hals, hassan-truscova, hihen, oualidpro, pfapostol, ravikiranweb3, slvDev, zhaojie
15.328 USDC - $15.33
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
Inconsistent rounding in _BASE_TARGET_
and _QUOTE_TARGET_
will deviate away from true targets and price curve.
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.
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)); }
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)