Skip to content

Conversation

@e1Ru1o
Copy link
Contributor

@e1Ru1o e1Ru1o commented Nov 6, 2025

100% price impact used as limit

@e1Ru1o e1Ru1o requested a review from duncancmt November 6, 2025 01:51
@e1Ru1o e1Ru1o self-assigned this Nov 6, 2025
Copy link
Collaborator

@duncancmt duncancmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still seems to me that there ought to be a more efficient way of conditionally clamping the stopping price between its min and max values than what has been implemented here. But if you've thought about it and can't come up with anything better, then I'll leave it be.

Can you also triple check that you're rounding in the correct direction with all your fixed-point approximations? I'm extra paranoid about this now after Bunni and BalancerV2.

How does the gas compare for this technique vs the previous technique of passing in the limit in calldata? Make sure to account for the increased intrinsic gas costs of the additional nonzero bytes of calldata. At least from spot checking a couple of snapshots, this seems like a pretty severe gas pessimization, much more than I would've anticipated. Do you know why?

// mask can only decrease if it is over 0. If it is not and priceSqrt is
// less than (1 << 62), then priceSqrt will be clamped later on to MIN_SQRT_RATIO
// as it will still be lower than (1 << 62)
if (mask > 0 && priceSqrt < (1 << 62)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

I wonder if this if/else tree can be more efficiently implemented. What does the reference do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the closest I was looking at in the reference, which also have several branches
image
But will double check if there is anything better I am missing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they uses the above everytime they do math with sqrtPrices. The match in our case can only move to the upper/lower class as it is just multiplying/dividing for sqrt(2), so I tried to just check that to adjust the result

@e1Ru1o
Copy link
Contributor Author

e1Ru1o commented Nov 7, 2025

Match was improved and comments made explicit

Copy link
Collaborator

@duncancmt duncancmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably need to make 1 more pass over this before approval. How's the gas consumption looking?

Copy link
Collaborator

@duncancmt duncancmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamkunal9's AI agent caught some bugs. Please thoroughly go back over your code for MaverickV2 and make sure there are no more lingering issues

uint256 spacing = pool.fastTickSpacing();
// everything is rounded down to ensure that the selected tick
// has at most 100% price impact
int256 delta = int256(6931 / spacing);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when spacing is full-range? I believe we end up with delta = 0 and then no swaps are possible. Even in the absence of this edge case, I think that this logic is not correct. Please refer back to the MaverickV2 documentation and fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't found anything about full range liquidity in Maverick docs, but I can tell that delta = 0 doesn't prevent a swap from happening (tried it to double check) as the price can move without moving the tick, and that is what we use in Maverick, tick, not price. If I round up what we can guarantee is that the impact is at most 200%

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially rounded up, but then wanted to enforce the at most 100% as everywhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you suspect it doesn't work due to something else please let me know

Copy link
Collaborator

@duncancmt duncancmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to go over Dodo, Maverick, Ekubo, and UniV2


uint256 limit = (!zeroForOne).ternary(uint256(1461446703485210103287273052203988822378723970341), uint256(4295128740));
(uint256 lo, uint256 hi) = zeroForOne.maybeSwap(priceSqrtX96, limit);
// All operations where rounded down to ensure that the selected price has at most 100% price impact
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this comment is justified. Can you explain more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔, it is kind of naive, the math was done to get to the price which exact 100% price impact, as this is exact math, so in floating math we will get to the exact price (==). But, as this is exact math, rounding down in the middle changes the inequality to less or equal (<=). That is why I said at most but we should not be able to reach the boundary

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to the price which exact 100% price impact

are you sure it's exactly 100% price impact? you're using fixnums, which are not typically exact and demand attention to rounding direction. given that the direction that the price moves depends on zeroForOne, I would expect the correct computation to require that we round up in one direction and round down in the other. I would not expect that rounding down gives the correct <= bounds in both cases.

of course, given that we're working with fairly large fixnum bases here (Q96 and Q95), it's possible that the error introduced by inconsistent rounding either doesn't practically matter or literally does not matter even at the extremes of sqrtPriceX96. it would be good to call this out explicitly in this comment.

Copy link
Collaborator

@duncancmt duncancmt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed everything except your changes to the DodoV1 action. I am having some trouble with that one figuring out whether what you've written is exactly equivalent to the old code. Can you walk me through your argument about why it's the same?

@e1Ru1o
Copy link
Contributor Author

e1Ru1o commented Nov 12, 2025

Let me list what I discover and did in DodoV1:

  1. There are 3 calculation functions, _RBelowSellQuoteToken, _RAboveSellQuoteToken, and _ROneSellQuoteToken
  2. Each of them have the calculation of uint256 i = DecimalMath.divFloor(DecimalMath.ONE, state.oraclePrice);. Duplicated code!, that was moved to the main function dodoQuerySellQuoteToken
  3. Two of them, _RAboveSellQuoteToken, and _ROneSellQuoteToken, are identical besides the second parameter passed to _SolveQuadraticFunctionForTrade which is also used in the return value. That is duplicated code, so they were merged.
  4. DodoState was used for simplicity to pass the data from a function to the other. This along with QUOTE_BALANCE (Q in the code) being used only in one of the branches of dodoQuerySellQuoteToken made me decide to remove the struct to avoid allocating unnecessary memory and move the call to fast_QUOTE_BALANCE_ to the branch where it is relevant.
  5. The branches with RStatus.ONE and RStatus.ABOVE_ONE were going to use the same function with just that parameter difference, so ternary was put in place to differentiate the parameter and the branches merged.
  6. The last branch left (else in the original code) which was meant to be for RStatus.BELOW_ONE was mostly kept as it is, and only the contents of the _RBelowSellQuoteToken were inlined in favor of removing the function as it was the only place where it was used.

@e1Ru1o e1Ru1o marked this pull request as ready for review November 12, 2025 14:09
@e1Ru1o e1Ru1o requested a review from dekz as a code owner November 12, 2025 14:09
@immunefi-magnus
Copy link

🛡️ Immunefi PR Reviews

We noticed that your project isn't set up for automatic code reviews. If you'd like this PR reviewed by the Immunefi team, you can request it manually using the link below:

🔗 Send this PR in for review

Once submitted, we'll take care of assigning a reviewer and follow up here.

@e1Ru1o e1Ru1o marked this pull request as draft November 12, 2025 14:09
@e1Ru1o e1Ru1o mentioned this pull request Nov 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants