Abracadabra Mimswap - Trust'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: 1/36

Findings: 8

Award: $14,067.26

🌟 Selected for report: 7

🚀 Solo Findings: 3

Findings Information

🌟 Selected for report: Trust

Also found by: ZanyBonzy, blutorque, ether_sky

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sponsor disputed
sufficient quality report
:robot:_31_group
H-01

Awards

1256.8429 USDC - $1,256.84

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/mimswap/MagicLP.sol#L564

Vulnerability details

Description

MagicLP provides a TWAP value which can be accessed via _BASE_PRICE_CUMULATIVE_LAST_

It is updated in the function below:

function _twapUpdate() internal { uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32); uint32 timeElapsed = blockTimestamp - _BLOCK_TIMESTAMP_LAST_; if (timeElapsed > 0 && _BASE_RESERVE_ != 0 && _QUOTE_RESERVE_ != 0) { /// @dev It is desired and expected for this value to /// overflow once it has hit the max of `type.uint256`. unchecked { _BASE_PRICE_CUMULATIVE_LAST_ += getMidPrice() * timeElapsed; } } _BLOCK_TIMESTAMP_LAST_ = blockTimestamp; }

It is updated by any function that changes the reserves, for example:

function _resetTargetAndReserve() internal returns (uint256 baseBalance, uint256 quoteBalance) { baseBalance = _BASE_TOKEN_.balanceOf(address(this)); quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this)); if (baseBalance > type(uint112).max || quoteBalance > type(uint112).max) { revert ErrOverflow(); } _BASE_RESERVE_ = uint112(baseBalance); _QUOTE_RESERVE_ = uint112(quoteBalance); _BASE_TARGET_ = uint112(baseBalance); _QUOTE_TARGET_ = uint112(quoteBalance); _RState_ = uint32(PMMPricing.RState.ONE); _twapUpdate(); } function _setReserve(uint256 baseReserve, uint256 quoteReserve) internal { _BASE_RESERVE_ = baseReserve.toUint112(); _QUOTE_RESERVE_ = quoteReserve.toUint112(); _twapUpdate(); }

The root cause of the issue is that the TWAP is updated after reserve changes. Since the TWAP multiplies the duration of time since the last update with the new reserves, an attacker has control over the registered price for the entire passed duration.

For reference, the Uniswap and Beanstalk TWAPs are provided below:

  • UniswapV2 - priceXCumulativeLast is written and then reserves are changed
  • Beanstalk - pumps are updated before the swap operation.

RareSkills details how TWAP operation works here.

Impact

Any application making use of the MagicLP's TWAP to determine token prices will be exploitable.

Proof of Concept

  1. At time T0, `BASE_PRICE_CUMULATIVE_LAST = X.
  2. Integrating contract records (T0,X)
  3. T seconds pass, without swaps, meaning price remained X.
  4. A contract queries _BASE_PRICE_CUMULATIVE_LAST_ to execute a large swap(A->B). The swap could be done through MIMswap or any other AMM.
  5. Attacker frontruns the query by inflating the A reserves with a large swap. New price is Y >> X.
  6. Integrating contract records (T0+T, X + Y * T)
  7. When calculating TWAP for last T seconds: (X+Y*T-X)/(T0+T-T0) = Y*T/T = Y. Attacker has succeeded in manipulating the price to Y.
  8. The victim performs a losing trade
  9. Attacker swaps back (B->A) to get back their tokens minus swap fees, profiting from the price manipulation.

Tools Used

Manual audit

_twapUpdate() needs to be called before reserves are updated.

Assessed type

Timing

#0 - 0xCalibur

2024-03-14T18:59:02Z

It's as designed. Integrating protocol should always check for min output to avoid frontrunning

#1 - c4-pre-sort

2024-03-15T12:57:51Z

141345 marked the issue as primary issue

#2 - 141345

2024-03-15T12:57:56Z

updated after reserve changes

#3 - c4-pre-sort

2024-03-15T12:57:58Z

141345 marked the issue as sufficient quality report

#4 - c4-sponsor

2024-03-28T17:58:24Z

0xCalibur (sponsor) disputed

#5 - thereksfour

2024-03-29T03:34:42Z

I think this is valid, the problem is that the TWAP algorithm is wrong, TWAP: https://en.wikipedia.org/wiki/Time-weighted_average_price By the way, the code here is consistent with DODOV2.

#6 - c4-judge

2024-03-29T05:13:42Z

thereksfour marked the issue as satisfactory

#7 - c4-judge

2024-03-31T06:25:33Z

thereksfour marked the issue as selected for report

#8 - 0xCalibur

2024-04-04T12:56:18Z

We decided to removed the TWAP functionality at the end. But during the time of this review, this was in the code

Findings Information

🌟 Selected for report: Trust

Labels

bug
3 (High Risk)
disagree with severity
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
upgraded by judge
H-02

Awards

6896.2571 USDC - $6,896.26

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/mimswap/MagicLP.sol#L381

Vulnerability details

Description

One of the two key parameters in MagicLP pools is I, which is defined to be the ideal ratio between the two reserves. It is set during MagicLP initialization: _I_ = i;

It is used when performing the initial LP deposit, in buyShares():

if (totalSupply() == 0) { // case 1. initial supply if (quoteBalance == 0) { revert ErrZeroQuoteAmount(); } shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) ? DecimalMath.divFloor(quoteBalance, _I_) : baseBalance; _BASE_TARGET_ = shares.toUint112(); _QUOTE_TARGET_ = DecimalMath.mulFloor(shares, _I_).toUint112(); if (_QUOTE_TARGET_ == 0) { revert ErrZeroQuoteTarget(); } if (shares <= 2001) { revert ErrMintAmountNotEnough(); } _mint(address(0), 1001); shares -= 1001;

The QUOTE_TARGET is determined by multiplying the BASE_TARGET with I.

The flaw is in the check below: shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) ? DecimalMath.divFloor(quoteBalance, _I_) : baseBalance; Essentially there needs to be enough quoteBalance at the I ratio to mint baseBalance shares, if there's not enough then shares are instead determined by dividing the quoteBalance with I. An attacker can abuse the mulFloor() to create a major inconsistency. Suppose quoteBalance = 1, baseBalance = 19999, I = 1e14. Then we have: 1 < 19999 * 1e14 / 1e18 => 1 < 1 => False Therefore shares = 19999 . It sets the targets:

_BASE_TARGET_ = 19999 _QUOTE_TARGET_ = 19999 * 1e14 / 1e18 = 1

The result is the ratio 1:19999, when the intended ratio from I is 1:1000.

Essentially a small rounding error is magnified. The rounding direction should instead be: quoteBalance < DecimalMath.mulCeil(baseBalance, _I_) This would ensure that DecimalMath.divFloor(quoteBalance, _I_) is executed. At this point, when calculating QUOTE_TARGET there will not be a precision error as above (it performs the opposing actions to the divFloor).

An attacker can abuse it by making users perform trades under the assumption I is the effective ratio, however the ratio is actually 2I. The pool's pricing mechanics will be wrong. Note that users will legitimately trust any MagicLP pool created by the Factory as it is supposed to enforce that ratio.

The attack can be performed on another entity's pool right after the init() call, or on a self-created pool. The initial cost for the attack is very small due to the small numbers involved.

Impact

Attacker can initialize a Pool with malicious pricing mechanics that break the assumed invariants of the pool, leading to incorrect pool interactions.

POC

Step by step of the initial buyShares() was provided above. _QUOTE_TARGET_ is used by the pricer:

function getPMMState() public view returns (PMMPricing.PMMState memory state) { state.i = _I_; state.K = _K_; state.B = _BASE_RESERVE_; state.Q = _QUOTE_RESERVE_; state.B0 = _BASE_TARGET_; // will be calculated in adjustedTarget state.Q0 = _QUOTE_TARGET_; state.R = PMMPricing.RState(_RState_); PMMPricing.adjustedTarget(state); } ... function sellBaseToken(PMMState memory state, uint256 payBaseAmount) internal pure returns (uint256 receiveQuoteAmount, RState newR) { if (state.R == RState.ONE) { // case 1: R=1 // R falls below one receiveQuoteAmount = _ROneSellBaseToken(state, payBaseAmount); newR = RState.BELOW_ONE; } else if (state.R == RState.ABOVE_ONE) { uint256 backToOnePayBase = state.B0 - state.B; uint256 backToOneReceiveQuote = state.Q - // @@@@@@@@@@ USED HERE state.Q0;

Note that deposits/withdrawals will continue to apply the bad ratio:

} else if (baseReserve > 0 && quoteReserve > 0) { // case 2. normal case 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();

Tools Used

Manual audit

Use DecimalMaths.mulCeil() to protect against the rounding error.

Assessed type

Math

#0 - 0xCalibur

2024-03-14T14:17:55Z

Based on our previous audit, it was discussed that using mulCeil here would not be the right answer since that would just create imprecision in the ratio in the other direction.

It would be good if the submitter could provide a PoC showing a veritable exploit case for this one.

#1 - 141345

2024-03-15T13:41:27Z

rounding could break I-invariant

need to check the dup

#2 - c4-pre-sort

2024-03-15T13:41:31Z

141345 marked the issue as sufficient quality report

#3 - c4-pre-sort

2024-03-15T13:41:38Z

141345 marked the issue as primary issue

#4 - c4-sponsor

2024-03-18T02:22:47Z

0xCalibur (sponsor) acknowledged

#5 - c4-sponsor

2024-03-18T17:15:28Z

0xCalibur (sponsor) disputed

#6 - c4-sponsor

2024-03-18T17:18:12Z

0xCalibur (sponsor) acknowledged

#7 - 0xCalibur

2024-03-19T14:39:36Z

Ack, but will not fix it on contract level but filtering pool quality and legitimacy on our main frontend.

#8 - c4-sponsor

2024-03-28T13:56:43Z

0xCalibur marked the issue as disagree with severity

#9 - thereksfour

2024-03-29T05:35:43Z

Downgraded to M based on the impact > Attacker can initialize a Pool with malicious pricing mechanics that break the assumed invariants of the pool, leading to incorrect pool interactions.

#10 - c4-judge

2024-03-29T05:37:22Z

thereksfour changed the severity to 2 (Med Risk)

#11 - c4-judge

2024-03-29T09:15:59Z

thereksfour marked the issue as satisfactory

#12 - c4-judge

2024-03-31T06:40:27Z

thereksfour marked the issue as selected for report

#13 - trust1995

2024-04-02T12:07:52Z

Hi,

The impact demonstrated is doubling the ratio of the pool, which is a core invariant of the MIMswap platform. It means pricing will be incorrect, which is the core functionality of an AMM. A user who will make a trade assuming they will follow the price set out by the I parameter will make incorrect trades, losing their funds inappropriately. I believe the direct risk of loss of funds by innocent traders who are not making a mistake, warrants the severity of High.

#14 - blutorque

2024-04-03T06:42:01Z

This findings should be invalid for 2 reasons;

  1. _I_ never changes during the above fuction call, it remains same as what admin set initially, meaning if a user performs trade, the shares amount received would only determined from _I_ previoulsy sets by admin, not an 2I which manipulated by attacker shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) ? DecimalMath.divFloor(quoteBalance, _I_) : baseBalance;

  2. the only things it will do, changing BASE_TARGET to QUOTE_TARGET ratio which protocol never uses during the trade. Instead, it uses the adjusted targets which solely determined from the base/quote reserve, not what warden mentioned above.

One can verified in code below, the adjusted targets are independent of current base/quote targets, https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/mimswap/libraries/PMMPricing.sol#L190-L201

Also in formulaes below, Screenshot 2024-04-03 120903 Screenshot 2024-04-03 115234

#15 - trust1995

2024-04-03T06:52:21Z

You can see the last section of the report no how deposits/withdrawals will use the bad ratio. You can also see that the state is used for pricing:

function getPMMState() public view returns (PMMPricing.PMMState memory state) { state.i = _I_; state.K = _K_; state.B = _BASE_RESERVE_; state.Q = _QUOTE_RESERVE_; state.B0 = _BASE_TARGET_; // will be calculated in adjustedTarget state.Q0 = _QUOTE_TARGET_; state.R = PMMPricing.RState(_RState_); PMMPricing.adjustedTarget(state); }
function querySellQuote( address trader, uint256 payQuoteAmount ) public view returns (uint256 receiveBaseAmount, uint256 mtFee, PMMPricing.RState newRState, uint256 newQuoteTarget) { PMMPricing.PMMState memory state = getPMMState(); // call below (receiveBaseAmount, newRState) = PMMPricing.sellQuoteToken(state, payQuoteAmount); (uint256 lpFeeRate, uint256 mtFeeRate) = _MT_FEE_RATE_MODEL_.getFeeRate(trader, _LP_FEE_RATE_); mtFee = DecimalMath.mulFloor(receiveBaseAmount, mtFeeRate); receiveBaseAmount = receiveBaseAmount - DecimalMath.mulFloor(receiveBaseAmount, lpFeeRate) - mtFee; newQuoteTarget = state.Q0; }
function sellQuoteToken(PMMState memory state, uint256 payQuoteAmount) internal pure returns (uint256 receiveBaseAmount, RState newR) { if (state.R == RState.ONE) { receiveBaseAmount = _ROneSellQuoteToken(state, payQuoteAmount); newR = RState.ABOVE_ONE; } else if (state.R == RState.ABOVE_ONE) { receiveBaseAmount = _RAboveSellQuoteToken(state, payQuoteAmount); newR = RState.ABOVE_ONE; } else { uint256 backToOnePayQuote = state.Q0 - state.Q; uint256 backToOneReceiveBase = state.B - state.B0; if (payQuoteAmount < backToOnePayQuote) { receiveBaseAmount = _RBelowSellQuoteToken(state, payQuoteAmount); newR = RState.BELOW_ONE; if (receiveBaseAmount > backToOneReceiveBase) { receiveBaseAmount = backToOneReceiveBase; } } else if (payQuoteAmount == backToOnePayQuote) { receiveBaseAmount = backToOneReceiveBase; newR = RState.ONE; } else { receiveBaseAmount = backToOneReceiveBase + _ROneSellQuoteToken(state, payQuoteAmount - backToOnePayQuote); newR = RState.ABOVE_ONE; } } }
function _RAboveSellQuoteToken( PMMState memory state, uint256 payQuoteAmount ) internal pure returns ( uint256 // receiveBaseToken ) { // Using both current price and target price return Math._SolveQuadraticFunctionForTrade(state.B0, state.B, payQuoteAmount, DecimalMath.reciprocalFloor(state.i), state.K); }

#16 - blutorque

2024-04-03T14:04:31Z

The attack can be performed on another entity's pool right after the init() call, or on a self-created pool. The initial cost for the attack is very small due to the small numbers involved.

This attack require to be performed right after the init() call, and we know that the initially pool are at equilibrium, means state.R==RState.ONE. However, in the PoC, warden pointed to the use of target to the section where state.R==ABOVE_ONE, which does not make any sense.

    if (state.R == RState.ONE) {
        // case 1: R=1
        // R falls below one
        receiveQuoteAmount = _ROneSellBaseToken(state, payBaseAmount);
        newR = RState.BELOW_ONE;
    } else if (state.R == RState.ABOVE_ONE) {
        uint256 backToOnePayBase = state.B0 - state.B;
        uint256 backToOneReceiveQuote = state.Q - 
		// @@@@@@@@@@   USED HERE 
		state.Q0;

#17 - trust1995

2024-04-03T14:17:25Z

The attack (buyShares() call) is done right after init().

The corrupted ratio will be used later in buyShares()/sellShares(), and trade calls after R moves to ABOVE_ONE/BELOW_ONE.

#18 - blutorque

2024-04-04T13:05:47Z

This is all going back to my first comment above, the affected ratio due to buyShares will re-adjust when trade occur and R get aways from ONE. No way the algorithm will let trader to use corrupted ratio.

#19 - thereksfour

2024-04-05T17:05:01Z

@0xCalibur Could you please give your opinion on the validity of this issue?

#20 - thereksfour

2024-04-06T06:07:43Z

  1. The attacker can compromise QUOTE_TARGET in buyShares() after the pool is created.
    function createPool(
        address baseToken,
        address quoteToken,
        uint256 lpFeeRate,
        uint256 i,
        uint256 k,
        address to,
        uint256 baseInAmount,
        uint256 quoteInAmount
    ) external returns (address clone, uint256 shares) {
        _validateDecimals(IERC20Metadata(baseToken).decimals(), IERC20Metadata(quoteToken).decimals());

        clone = IFactory(factory).create(baseToken, quoteToken, lpFeeRate, i, k);

        baseToken.safeTransferFrom(msg.sender, clone, baseInAmount);
        quoteToken.safeTransferFrom(msg.sender, clone, quoteInAmount);
        (shares, , ) = IMagicLP(clone).buyShares(to);  <========
    }
  1. The victim calls sellBase, and the call chain is as follows. In adjustedTarget, state.Q0 will not be adjusted since state.R == RState.ONE.
    function sellBase(address to) external nonReentrant returns (uint256 receiveQuoteAmount) {
        uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
        uint256 baseInput = baseBalance - uint256(_BASE_RESERVE_);
        uint256 mtFee;
        uint256 newBaseTarget;
        PMMPricing.RState newRState;
        (receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput); <==============
...
    function querySellBase(
        address trader,
        uint256 payBaseAmount
    ) public view returns (uint256 receiveQuoteAmount, uint256 mtFee, PMMPricing.RState newRState, uint256 newBaseTarget) {
        PMMPricing.PMMState memory state = getPMMState(); <========
        (receiveQuoteAmount, newRState) = PMMPricing.sellBaseToken(state, payBaseAmount); <======
...
    function getPMMState() public view returns (PMMPricing.PMMState memory state) {
        state.i = _I_;
        state.K = _K_;
        state.B = _BASE_RESERVE_;
        state.Q = _QUOTE_RESERVE_;
        state.B0 = _BASE_TARGET_; // will be calculated in adjustedTarget
        state.Q0 = _QUOTE_TARGET_;
        state.R = PMMPricing.RState(_RState_);
        PMMPricing.adjustedTarget(state); <======
    }
...
    function adjustedTarget(PMMState memory state) internal pure {
        if (state.R == RState.BELOW_ONE) {
            state.Q0 = Math._SolveQuadraticFunctionForTarget(state.Q, state.B - state.B0, state.i, state.K);
        } else if (state.R == RState.ABOVE_ONE) {
            state.B0 = Math._SolveQuadraticFunctionForTarget(
                state.B,
                state.Q - state.Q0,
                DecimalMath.reciprocalFloor(state.i),
                state.K
            );
        }
    }
  1. sellBaseToken calls _ROneSellBaseToken, and _ROneSellBaseToken calls _SolveQuadraticFunctionForTrade to use compromised QUOTE_TARGET for calculation. It'll compromise the victim.
    function sellBaseToken(PMMState memory state, uint256 payBaseAmount) internal pure returns (uint256 receiveQuoteAmount, RState newR) {
        if (state.R == RState.ONE) {
            // case 1: R=1
            // R falls below one
            receiveQuoteAmount = _ROneSellBaseToken(state, payBaseAmount); <======
            newR = RState.BELOW_ONE;
...
    function _ROneSellBaseToken(
        PMMState memory state,
        uint256 payBaseAmount
    )
        internal
        pure
        returns (
            uint256 // receiveQuoteToken
        )
    {
        // in theory Q2 <= targetQuoteTokenAmount
        // however when amount is close to 0, precision problems may cause Q2 > targetQuoteTokenAmount
        return Math._SolveQuadraticFunctionForTrade(state.Q0, state.Q0, payBaseAmount, state.i, state.K); <======
    }

@blutorque @trust1995 If the above is correct, I would upgrade it to H

#21 - c4-judge

2024-04-06T07:22:24Z

thereksfour changed the severity to 3 (High Risk)

#22 - blutorque

2024-04-07T20:22:40Z

hey @thereksfour, I agree, you are right above.

After giving one more last thought to it. I realized, Is there going to be enough liquidity in quoteToken for swap, when trader sellBaseToken. Its obvious, there wont.

And even if someone do provide liquidity keeping state.R==ONE, the ratio will not be same anymore.

#23 - thereksfour

2024-04-08T03:33:05Z

Sorry, the judging results are already wrapped up. Also, it seems that the attacker can add liquidity without changing state by donating tokens and then calling sync

Findings Information

🌟 Selected for report: Trust

Also found by: ether_sky

Labels

bug
2 (Med Risk)
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
sufficient quality report
:robot:_92_group
M-03

Awards

930.9947 USDC - $930.99

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/mimswap/periphery/Router.sol#L515

Vulnerability details

Description

Users are expected to add liquidity through addLiquidity() of the Router. It receives base and quote amounts which are adjusted through _adjustAddLiquidity():

function addLiquidity( address lp, address to, uint256 baseInAmount, uint256 quoteInAmount, uint256 minimumShares, uint256 deadline ) external ensureDeadline(deadline) returns (uint256 baseAdjustedInAmount, uint256 quoteAdjustedInAmount, uint256 shares) { (baseAdjustedInAmount, quoteAdjustedInAmount) = _adjustAddLiquidity(lp, baseInAmount, quoteInAmount); IMagicLP(lp)._BASE_TOKEN_().safeTransferFrom(msg.sender, lp, baseAdjustedInAmount); IMagicLP(lp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, lp, quoteAdjustedInAmount); shares = _addLiquidity(lp, to, minimumShares); }

The adjustment function has DODOv2 code which determines the effective base:quote ratio and adjusts the higher of the two input values so that no input is wasted. Additionally Abrakadbra inserted the following logic at the start of the function:

(uint256 baseReserve, uint256 quoteReserve) = IMagicLP(lp).getReserves(); uint256 baseBalance = IMagicLP(lp)._BASE_TOKEN_().balanceOf(address(lp)) + baseInAmount; uint256 quoteBalance = IMagicLP(lp)._QUOTE_TOKEN_().balanceOf(address(lp)) + quoteInAmount; baseInAmount = baseBalance - baseReserve; quoteInAmount = quoteBalance - quoteReserve;

The intention is that if the current balance of token X is higher than reserve, the input should be reduced so that only the delta needs to be sent. However the logic is miswritten. The balanceOf() and getReserve() calls have been reversed. The correct logic should be: requested amount = reserves + requested amount - balance Instead it is: requested amount = balance + requested amount - reserves. The higher the current balanceOf(), the higher the final requested amount becomes. This is a critical issue because tokens can be donated to the MagicLP by an attacker, making the victim send more tokens than expected. The POC gives an example of such a sequence.

Essentially this makes addLiquidity(amountX, amountY, minShares) become: (amountX2, amountY2, minShares) where amountX2 > amountX, amountY2 > amountY. By definition this is unauthorized use of the victim's funds.

Impact

Unauthorized spending of victim's tokens, leading to monetary losses.

Proof of Concept

  1. MagicLP has (500 USDC, 500 DAI) reserves and balances, 100 shares
  2. Victim calls addLiquidity for (1000 USDC, 1000 DAI) , min 200 shares
  3. Attacker frontrans and donates (1000 USDC, 500 DAI) to the LP
  4. addLiquidity() executes:
  • baseInAmount = 1500 + 1000 - 500 = 2000 USDC
  • quoteInAmount = 1000 + 1000 - 500 = 1500 DAI
  • baseIncreaseRatio = 2000 / 500 = 4
  • quoteIncreaseRatio = 1500 / 500 = 3
  • quoteAdjustedInAmount = 1500 DAI
  • baseAdjustedInAmount = 500 * 3 = 1500 USDC

Therefore LP::buyShares() is called with (1500, 1500) for 200 shares instead of (1000, 1000)

User has now put an extra ($500,$500) of unauthorized funds in the contract. They may or may not be aware of it when the TX is executed. Note that funds put in the LP are obviously in risk of impermanent loss alongside any other economic, systemic and security-related risks. This is a simple example to prove the point but could be tweaked for different tokens (more or less volatile), amounts and ratios.

Tools Used

Manual audit

Change the usage of getReserves() and balanceOf() in the lines below as described:

(uint256 baseReserve, uint256 quoteReserve) = IMagicLP(lp).getReserves(); uint256 baseBalance = IMagicLP(lp)._BASE_TOKEN_().balanceOf(address(lp)) + baseInAmount; uint256 quoteBalance = IMagicLP(lp)._QUOTE_TOKEN_().balanceOf(address(lp)) + quoteInAmount;

A great extra invariant check would be to require in addLiquidity() that the results baseAdjustedInAmount, quoteAdjustedInAmount are smaller than the original input amounts respectively.

Note that the issue also occurs in the preview function:

function previewAddLiquidity( address lp, uint256 baseInAmount, uint256 quoteInAmount ) external view returns (uint256 baseAdjustedInAmount, uint256 quoteAdjustedInAmount, uint256 shares) { (uint256 baseReserve, uint256 quoteReserve) = IMagicLP(lp).getReserves(); uint256 baseBalance = IMagicLP(lp)._BASE_TOKEN_().balanceOf(address(lp)) + baseInAmount; uint256 quoteBalance = IMagicLP(lp)._QUOTE_TOKEN_().balanceOf(address(lp)) + quoteInAmount; baseInAmount = baseBalance - baseReserve; quoteInAmount = quoteBalance - quoteReserve;

It should also be fixed here. We view it as the same root cause, but of a lesser end impact.

Assessed type

Math

#0 - c4-pre-sort

2024-03-15T13:26:30Z

141345 marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-03-15T13:26:34Z

141345 marked the issue as primary issue

#2 - 141345

2024-03-15T13:26:40Z

addLiquidity miscalculation, misuse of balanceOf() and getReserve()

#3 - 0xCalibur

2024-03-15T21:01:55Z

#4 - c4-sponsor

2024-03-15T21:02:07Z

0xCalibur (sponsor) confirmed

#5 - thereksfour

2024-03-28T16:56:19Z

Referring to #92, consider downgrade to M. This is more of a griefing attack, and it seems the victim's 200 share would be worth 2000 USDC, 1666 DAI?

#6 - c4-judge

2024-03-28T16:56:27Z

thereksfour changed the severity to 2 (Med Risk)

#7 - c4-judge

2024-03-29T08:30:36Z

thereksfour marked the issue as satisfactory

#8 - c4-judge

2024-03-29T15:15:10Z

thereksfour marked the issue as duplicate of #92

#9 - trust1995

2024-04-02T13:14:51Z

Referring to #92, consider downgrade to M. This is more of a griefing attack, and it seems the victim's 200 share would be worth 2000 USDC, 1666 DAI?

Unauthorized spending of tokens is breaking of a core invariant (do not use money user did not intend to spend), which by itself achieves High impact. The submission also explains the risks affecting the unapproved funds.

Note that sponsor has confirmed the issue at High severity and views it as such.

#10 - thereksfour

2024-04-05T09:39:25Z

Hey @trust1995, in POC, if I understand correctly, while the victim spends more tokens, the shares the victim receives will be worth more due to the donations of the griefer (3000 -> 3666), and there doesn't seem to be any locks here, so I don't think this as a higher impact than DOS.

As for the high severity confirmed by sponsors, I'd like to hear from the sponsors. Thanks @0xCalibur

#11 - trust1995

2024-04-05T12:33:36Z

I can confirm that the user spends more tokens and receives more shares. In my opinion that suffices for H severity as it is not blocking a victim interaction, it is taking unapproved funds (until the user figures out how to redeem them back). Again, touching user's assets without their permission is very severe.

#12 - thereksfour

2024-04-05T17:11:11Z

In terms of this attack scenario

  1. The attacker needs to donate funds
  2. the victim spends more funds than expected
  3. The victim receives a portion of the attacker's donated funds
  4. the user who added the liquidity should know how to get them back
  5. victims can get more money back immediately

so I don't think this is high severity

#13 - c4-judge

2024-04-06T08:39:54Z

thereksfour marked the issue as selected for report

#14 - thereksfour

2024-04-06T08:43:34Z

Make this as selected report based on the focus of the description

Findings Information

🌟 Selected for report: Trust

Also found by: SpicyMeatball, hals

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
:robot:_58_group
M-04

Awards

558.5968 USDC - $558.60

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/blast/BlastOnboardingBoot.sol#L142

Vulnerability details

Description

The BlastOnboardingBoot contract contains the bootstraping funds. After launch it will start a reward staking pool and allow users to claim tokens into it. This is in the launch sequence:

staking = new LockingMultiRewards(pool, 30_000, 7 days, 13 weeks, address(this)); staking.setOperator(address(this), true); staking.transferOwnership(owner); // Approve staking contract pool.safeApprove(address(staking), totalPoolShares);

Note that the code approves the staking pool towithdraw from the created LP. The code also provisions setting of the staking contract to another one.

// Just in case we need to change the staking contract after // the automatic bootstrapping process function setStaking(LockingMultiRewards _staking) external onlyOwner { if (ready) { revert ErrCannotChangeOnceReady(); } staking = _staking; emit LogStakingChanged(address(_staking)); }

The issue is that this function lacks two actions:

  • Resetting the previous staking pool's approval to zero
  • More importantly, approving the new staking pool all the bootstrap pool shares

As a result, owner which assumes changing to a new staking pool is perfectly safe, will in fact cause reverts when users try claiming in the new pool. It is still possible to revert back to the old staking pool, but in fact it will never be possible to migrate to a new pool as no other approve() call exists in the contract.

Impact

Loss of assumed functionality of the Onboarding contract in a highly-sensitive area.

Tools Used

Manual audit

  • Approve the new contract
  • Revoke approval from the previous contract for good sanitization

Assessed type

ERC20

#0 - c4-pre-sort

2024-03-15T13:27:40Z

141345 marked the issue as primary issue

#1 - 141345

2024-03-15T13:27:40Z

change staking contract, miss approve update and reset

#2 - c4-pre-sort

2024-03-15T13:27:43Z

141345 marked the issue as sufficient quality report

#3 - 0xCalibur

2024-03-17T13:12:07Z

we can also upgrade the bootstrapper if we were to use the function. I agree with the finding but we just decided to remove the function setStaking

#4 - c4-sponsor

2024-03-17T13:12:09Z

0xCalibur (sponsor) acknowledged

#5 - c4-judge

2024-03-29T08:40:45Z

thereksfour marked the issue as satisfactory

#6 - c4-judge

2024-03-31T06:31:23Z

thereksfour marked the issue as selected for report

Findings Information

🌟 Selected for report: Trust

Labels

bug
2 (Med Risk)
disagree with severity
primary issue
satisfactory
selected for report
sponsor acknowledged
sufficient quality report
M-05

Awards

2068.8771 USDC - $2,068.88

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/staking/LockingMultiRewards.sol#L155

Vulnerability details

Description

The LockingMultiRewards allows anyone to stake MagicLP tokens in return for rewards. LP tokens are sent with either functions below:

function stake(uint256 amount, bool lock_) public whenNotPaused { _stakeFor(msg.sender, amount, lock_); } /// @notice Locks an existing unlocked balance. function lock(uint256 amount) public whenNotPaused { if (amount == 0) { revert ErrZeroAmount(); } _updateRewardsForUser(msg.sender); _balances[msg.sender].unlocked -= amount; unlockedSupply -= amount; _createLock(msg.sender, amount); }

This will lead to _createLock(), which calculates the nextUnlockTime():

function _createLock(address user, uint256 amount) internal { Balances storage bal = _balances[user]; uint256 _nextUnlockTime = nextUnlockTime();
function nextUnlockTime() public view returns (uint256) { return nextEpoch() + lockDuration; }

Note that nextEpoch() would always return the next 1-week (or reward duration) slot.

function epoch() public view returns (uint256) { return (block.timestamp / rewardsDuration) * rewardsDuration; } function nextEpoch() public view returns (uint256) { return epoch() + rewardsDuration; }

An issue arises because the user cannot specify the latest desired unlock time. This opens the path for the tokens to be locked for longer than expected, which may have significant impact for users if they need the funds. Consider the following case, where x is week number:

  • It is day 7x + 6 (one day from nextEpoch)
  • Assumed unlock time is 7x + 7 + lockDuration
  • The TX does not execute in next 1 day for any reason (gas price went up, validator does not include TX, etc)
  • It is now day 7x+7, another epoch starts. Now nextEpoch is 7x + 14
  • Executed unlock time is 7x + 14 + lockDuration

This means user's funds are locked for an additional 7 days more than expected.

Note that delayed execution of a user's TX has always been considered in scope, certainly for Med severity impacts: - https://solodit.xyz/issues/m-13-interactions-with-amms-do-not-use-deadlines-for-operations-code4rena-paraspace-paraspace-contest-git - https://solodit.xyz/issues/m-04-lack-of-deadline-for-uniswap-amm-code4rena-asymmetry-finance-asymmetry-contest-git - https://solodit.xyz/issues/m-03-missing-deadline-param-in-swapexactamountout-allowing-outdated-slippage-and-allow-pending-transaction-to-be-executed-unexpectedly-code4rena-pooltogether-pooltogether-git

Essentially the locking function lacks a deadline parameter similar to swapping functions, and the impact is temporary freeze of funds.

Impact

A user's tokens could be locked for an extended duration beyond their intention and without their control.

Tools Used

Manual audit

Consider adding a latestUnlockTime deadline parameter for the locking functions.

Assessed type

Timing

#0 - c4-pre-sort

2024-03-15T12:35:26Z

141345 marked the issue as primary issue

#1 - 141345

2024-03-15T12:35:33Z

extend lock by others

#2 - c4-pre-sort

2024-03-15T12:35:36Z

141345 marked the issue as sufficient quality report

#3 - c4-sponsor

2024-03-15T12:54:04Z

0xCalibur (sponsor) acknowledged

#5 - c4-sponsor

2024-03-15T16:01:16Z

0xCalibur marked the issue as disagree with severity

#6 - 0xCalibur

2024-03-15T16:01:25Z

Should be a low

#7 - thereksfour

2024-03-29T09:07:35Z

Here the latest discussion on transaction delays. https://github.com/code-423n4/2024-02-uniswap-foundation-findings/issues/331#issuecomment-2021292618

And I would consider it QA which is due to low likelihood and medium impact (temporary freeze of funds)

#8 - c4-judge

2024-03-29T09:07:48Z

thereksfour changed the severity to QA (Quality Assurance)

#9 - trust1995

2024-04-02T12:26:11Z

Hi,

I would like to argue that likelihood is medium and impact is high. likelihood - Assuming there will be many stakers on the platform, at any moment there could be a request close to the end of epoch. The closer the request is to the end of an epoch, the more likely it is a TX will not be executed until the next one. Additionally, the users of the stake() function are completely different in understanding/sophistication from the ones in the Unistaker contest. They cannot be assumed to understand TX delays, requirement of invalidating one's TX if it is undesirable, etc. Additionally one needs to take into consideration that gas prices are fluctuating, so censorship of a TX is absolutely not required for the FoF to occur. It just needs to be on a local low-point for the duration until the next interval for the impact to be achieved. Again, consider that this is not a targetted attack , the issue is a constant probabilistic leak that given enough time will materialize.

For impact, temporary FoF is a very serious issue, on the low end of the High impact (imo). Consider for example that a user assumes their tokens will be available at the unlock period in order to pay off a loan, and because of the FoF they will now default. This is just one example of hundred of different very serious scenarios that would occur to a user that assumes funds will be available in the future.

Considering all the arguments above, I believe the finding to be between M and H severity, rather than between L and M severity.

#10 - thereksfour

2024-04-04T15:59:32Z

Agreed, the lock duration is jumping, which may compromise user

#11 - c4-judge

2024-04-05T07:27:45Z

This previously downgraded issue has been upgraded by thereksfour

#12 - c4-judge

2024-04-05T07:30:13Z

thereksfour marked the issue as satisfactory

#13 - c4-judge

2024-04-05T07:30:19Z

thereksfour marked the issue as selected for report

Findings Information

🌟 Selected for report: Trust

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor disputed
sufficient quality report
:robot:_56_group
M-06

Awards

2068.8771 USDC - $2,068.88

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/oracles/aggregators/MagicLpAggregator.sol#L37

Vulnerability details

Description

MagicLpAggregator is used to price LP tokens for "closely-tied" underlying tokens. It calculates the price below:

function latestAnswer() public view override returns (int256) { uint256 baseAnswerNomalized = uint256(baseOracle.latestAnswer()) * (10 ** (WAD - baseOracle.decimals())); uint256 quoteAnswerNormalized = uint256(quoteOracle.latestAnswer()) * (10 ** (WAD - quoteOracle.decimals())); uint256 minAnswer = baseAnswerNomalized < quoteAnswerNormalized ? baseAnswerNomalized : quoteAnswerNormalized; (uint256 baseReserve, uint256 quoteReserve) = _getReserves(); baseReserve = baseReserve * (10 ** (WAD - baseDecimals)); quoteReserve = quoteReserve * (10 ** (WAD - quoteDecimals)); return int256(minAnswer * (baseReserve + quoteReserve) / pair.totalSupply()); }

The code takes the minimal answer between the underlying oracles and considers all reserves to be worth that amount: return int256(minAnswer * (baseReserve + quoteReserve) / pair.totalSupply());

The issue is that any difference in price between the assets represents an easy arbitrage opportunity. Suppose we have tokens (A,B), where real oracle shows:

  • A = $0.99
  • B = $1

The Pool has 1000000 LP tokens and contains:

  • 1000000 A
  • 1000000 B

The LP value would calculate as: 0.99 * 2000000 / 1000000 = $1.98 The actual value is: (0.99 * 1000000 + 1 * 1000000) / 1000000 = $1.99

Suppose a platform trades LPs using the aggregator pricing. An attacker could:

  • Buy 100,000 LP tokens at $198000
  • Withdraw from the pool the underlying shares
  • Sell 100,000 A, 100,000 B at $199000
  • Profit $1000 from the exchange, when the difference is just $0.01 (this is very common fluctuation even with pegged assets).

The delta comes at the expense of LP holders whose position gets minimized.

Impact

Loss of value due to arbitrage of any platform using MagicLpAggregator pricing.

Tools Used

Manual audit

Always calculate the value based on the real underlying token value multiplied by amount.

Consider creating two separate oracles for lower-bound and upper-bound results. Then a lending protocol could indeed use the lower-bound for determining collateral value.

Assessed type

MEV

#0 - 0xm3rlin

2024-03-15T00:35:15Z

Intended behavior, disputed

#1 - c4-pre-sort

2024-03-15T14:34:05Z

141345 marked the issue as primary issue

#2 - c4-pre-sort

2024-03-15T14:34:13Z

141345 marked the issue as sufficient quality report

#3 - 141345

2024-03-15T14:34:51Z

rounding error could accumlate in MagicLpAggregator

#4 - c4-sponsor

2024-03-28T17:58:15Z

0xCalibur (sponsor) disputed

#5 - c4-judge

2024-03-29T09:15:17Z

thereksfour marked the issue as satisfactory

#6 - c4-judge

2024-03-31T07:18:30Z

thereksfour marked the issue as selected for report

Findings Information

🌟 Selected for report: Trust

Also found by: AgileJune, Bigsam, Neon2835, grearlake

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
:robot:_44_group
M-07

Awards

271.4781 USDC - $271.48

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/staking/LockingMultiRewards.sol#L388

Vulnerability details

Description

The LockingMultiRewards contract facilitates distribution of rewards across the remaining epoch duration. The calculation for reward rate is provided below: reward.rewardRate = amount / _remainingRewardTime;

The rate is later multiplied by the duration to get the total rewards for elapsed time, demonstrated below:

function rewardsForDuration(address rewardToken) external view returns (uint256) { return _rewardData[rewardToken].rewardRate * rewardsDuration; }

An issue occurs because there is no sufficient wrapping of the amount before dividing by _remainingRewardTime. The number is divided and later multiplied by elapsed time, causing a loss of precision of the amount modulo remaining time. For the provided period of 1 week by the sponsor, the maximum amount lost can be calculated: 7 * 24 * 3600 - 1 = 604799. Note that the average case loss is half of the worst case assuming even distribution across time. However since rewards are usually not sent at the low end of remaining time (see the minRemainingTime variable, the actual average would be higher).

The effect of this size of loss depends on the decimals and value of the reward token. For USDC, this would be $0.6. For WBTC, it would be $423 (at time of writing). The loss is shared between all stakers relative to their stake amount. The loss occurs for every notification, so it is clear losses will be severe.

Sponsor remarked in the channel that reward tokens would be real, popular tokens. We believe USDC / WBTC on ARB are extremely popular tokens and therefore remain fully in scope as reward tokens.

Severity Rationalization

Impact - high Likelihood - medium -> Severity - high

Impact

Permanent loss of yield for stakers in reward pools due to precision loss.

Proof of Concept

  • Reward pool offers WBTC rewards
  • Epoch has just started,

Tools Used

Manual audit

Store the rewardRate scaled by 1e18, so loss of precision will be lower by magnitude of 1e18.

Assessed type

Math

#0 - 0xm3rlin

2024-03-15T00:15:38Z

irrelevant numbers of 6 USDC if using USDC

#1 - c4-pre-sort

2024-03-15T12:55:20Z

141345 marked the issue as duplicate of #166

#2 - c4-judge

2024-03-29T13:30:35Z

thereksfour changed the severity to QA (Quality Assurance)

#3 - trust1995

2024-04-06T08:44:10Z

Hi,

The duplicate has been closed due to loss of "dust amounts". However I show in my submission that it is far from dust, I believe any submissions that have identified material loss of funds should be awarded appropriately.

#4 - c4-judge

2024-04-07T08:52:28Z

This previously downgraded issue has been upgraded by thereksfour

#5 - c4-judge

2024-04-07T08:52:59Z

thereksfour marked the issue as satisfactory

#6 - c4-judge

2024-04-07T08:53:05Z

thereksfour marked the issue as selected for report

Awards

15.328 USDC - $15.33

Labels

bug
downgraded by judge
grade-b
primary issue
QA (Quality Assurance)
sufficient quality report
Q-02

External Links

Lines of code

https://github.com/code-423n4/2024-03-abracadabra-money/blob/1f4693fdbf33e9ad28132643e2d6f7635834c6c6/src/mimswap/MagicLP.sol#L250

Vulnerability details

Description

The MagicLP contract implements swapping through the sellBase() and sellQuote() functions. It calculates the out amount based on the input amount and the tx.origin:

(receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput);

The origin is passed as trader to the getFeeRate() function to get the custom lpFeeRate, mtFeeRate for that user:

function querySellBase( address trader, uint256 payBaseAmount ) public view returns (uint256 receiveQuoteAmount, uint256 mtFee, PMMPricing.RState newRState, uint256 newBaseTarget) { PMMPricing.PMMState memory state = getPMMState(); (receiveQuoteAmount, newRState) = PMMPricing.sellBaseToken(state, payBaseAmount); // Passed here @@@@@@@ (uint256 lpFeeRate, uint256 mtFeeRate) = _MT_FEE_RATE_MODEL_.getFeeRate(trader, _LP_FEE_RATE_); mtFee = DecimalMath.mulFloor(receiveQuoteAmount, mtFeeRate); receiveQuoteAmount = receiveQuoteAmount - DecimalMath.mulFloor(receiveQuoteAmount, lpFeeRate) - mtFee; newBaseTarget = state.B0; }

The idea to use tx.origin is due to the expected interaction with the contract through the Router periphery contract, in this case the msg.sender is not an accurate representation of the trader.

The issue is that when the trade is executed from a smart contract wallet, for example Gnosis Safe, tx.origin is at the control of the attacker. They may observe the execTransaction() call in the mempool and submit it by themselves, copying the parameters including the signatures. Control of the tx.origin grants the attacker control over the lpFeeRate and mtFeeRate charged in the TX. This means that the victim could assume a certain fee for the swap, while they would be overcharged due to an attacker frontrunning the TX. Note that this attack works both on direct interactions with the MagicLP or TXs through the Router contract.

Impact

When a trader swaps from a smart contract wallet, anyone could make them lose additional value through the trade.

Proof of Concept

  1. A smart contract wallet executes a swap through Router's sellBaseTokensForTokens()
  2. An attacker views the call in the mempool and executes the call from their own EOA / smart contract.
  3. The registered tx.origin for fee determination will be the attacker instead of the real sender.
  4. Extra fees will be absorbed by the protocol leading to loss of value for the trader.

Severity Rationalization

  • The prerequisite that victim must be a smart contract wallet is not a limiting condition, because it is extremely likely smart contract wallets, or smart contracts integrating with MIMswap, will be used.
  • The FeeRateModel implementation example ignores the trader passed in the second argument. However that is not valid grounds for downgrading since the issue is in the MagicLP contract, which cannot make any assumptions on the non-usage of any passed parameters. It is very reasonable to assume the trader parameter would be used, otherwise it wouldn't be passed to the function in a newly-written codebase.

Without severity reduction from above it seems clear the loss of capital translates to high-severity.

Tools Used

Manual audit

Abolish the use of tx.origin. The Router can pass the msg.sender down to the MagicLP contract to determine the intended trader.

Note that the issue extends also to the flashloan() function, to a lesser impact.

Assessed type

Invalid Validation

#0 - 0xCalibur

2024-03-14T18:29:00Z

It's by design and we don't plan to be using the trader parameter but If we were to use it we would take extra precautious by whitelisting allowed addresses.

#1 - c4-pre-sort

2024-03-15T14:32:34Z

141345 marked the issue as sufficient quality report

#2 - 141345

2024-03-15T14:32:42Z

tx.origin exploit need to take a close look at the POC

#3 - c4-pre-sort

2024-03-15T14:32:46Z

141345 marked the issue as primary issue

#4 - thereksfour

2024-03-28T16:38:59Z

The victim is smart wallet reduces its likelihood, and the issue has predictions of future code which further reduces its likelihood and impact. All things considered I would downgrade it to QA

#5 - c4-judge

2024-03-28T16:39:09Z

thereksfour changed the severity to QA (Quality Assurance)

#6 - trust1995

2024-04-02T13:37:12Z

Hi,

The baseline impact we are dealing with is loss of funds due to paying more fees than intended (High). I have made a case why speculation of future code is valid in this case in the description, but I agree it decreases likelihood somewhat (It does not decrease impact though). I don't think use of smart contract wallets should be out of scope since the contracts never check it and there aren't any remarks around that option anywhere. Users should be free to use whatever compliant wallet they would like while working with the contract safely. All things considered it seems the lowest the finding could get downgraded to is Medium.

#7 - thereksfour

2024-04-05T15:14:15Z

I still think predicting future code will reduce the impact of the issue, or it will directly reduce the severity of the issue.

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