Skip to content

Refactor and optimize SwapMath#280

Merged
hensha256 merged 23 commits into
Uniswap:mainfrom
Aperture-Finance:optimize-swapmath
May 26, 2024
Merged

Refactor and optimize SwapMath#280
hensha256 merged 23 commits into
Uniswap:mainfrom
Aperture-Finance:optimize-swapmath

Conversation

@shuhuiluo

Copy link
Copy Markdown
Contributor

Related Issue

Which issue does this pull request resolve?

Obscure control flow related to SwapMath.

Description of changes

Improvements to the SwapMath library have been implemented. A new function, getSqrtRatioTarget, written in Yul using bitwise operations, has been introduced to replace a nested ternary operation within Pool.swap:

(
    params.zeroForOne
        ? step.sqrtPriceNextX96 < params.sqrtPriceLimitX96
        : step.sqrtPriceNextX96 > params.sqrtPriceLimitX96
) ? params.sqrtPriceLimitX96 : step.sqrtPriceNextX96

which basically was doing

zeroForOne ? max(sqrtPriceNextX96, sqrtPriceLimitX96) : min(sqrtPriceNextX96, sqrtPriceLimitX96)

Furthermore, the computeSwapStep function has been refactored, resulting in a cleaner control flow and significant gas savings. The gas savings achieved range from 10% to 28%, as can be seen in SwapMath.spec.ts.snap. In conjunction with the changes introduced in pull request #258, the maximum savings on computeSwapStep can reach up to 40%.

All tests pass except for SwapMath::#computeSwapStep::entire input amount taken as fee, where amountIn changes from 0 to 9, and feeAmount changes from 10 to 1.

I wonder if "entire input amount taken as fee when output is zero" is an intended feature and the invariant amountIn == 0 && feeAmount == 10 should be kept. Adjusting the invariant to amountIn + feeAmount == 10 appears to be acceptable and reasonable.

@hensha256

Copy link
Copy Markdown
Contributor

Please can you merge main so we can re-review the gas improvements 🙏

# Conflicts:
#	.forge-snapshots/simple swap.snap
#	.forge-snapshots/swap against liquidity with native token.snap
#	.forge-snapshots/swap against liquidity.snap
#	.forge-snapshots/swap with hooks.snap
#	.forge-snapshots/swap with native.snap
#	src/libraries/SwapMath.sol
#	test/SwapMath.spec.ts
#	test/__snapshots__/PoolManager.gas.spec.ts.snap
#	test/__snapshots__/PoolManager.spec.ts.snap
#	test/__snapshots__/SwapMath.spec.ts.snap
@shuhuiluo

Copy link
Copy Markdown
Contributor Author

Please can you merge main so we can re-review the gas improvements 🙏

@hensha256 Done. As stated before, I had to modify test_entireInputAmountTakenAsFee, which I believe to be an artifact of the previous control flow instead of an intended feature.

Replace inline assembly with native Solidity for `amountRemainingAbs` calculation in `computeSwapStep` function. The previous implementation was not intuitive and easy to read. This change simplifies the code and improves readability while maintaining the exact functionality.
In `SwapMath.sol`, we reduced redundancy by storing the value of `feePips` into `_feePips` and using it throughout the function instead of casting `feePips` multiple times. This refactoring improved efficiency and reduced bytecode size, leading to reduced gas cost for function calls.
The declaration and assignment of `amountRemainingAbs` has been moved inside the relevant if-else blocks in the SwapMath.sol file. This change makes the code more concise and clear by limiting the scope and use of `amountRemainingAbs` to where it is needed.
Comment thread src/libraries/SwapMath.sol Outdated
// cap the output amount to not exceed the remaining output amount
amountOut = uint256(amountRemaining);
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput(
sqrtPriceCurrentX96, liquidity, uint256(amountRemaining), zeroForOne

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is it cheaper to pass in amountOut to prevent double-casting amountRemaining?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Casting between uint256 and int256 shouldn't matter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

was gonna ask same thing - even if it doesn't make a difference I think the code reads better with using the variable we just defined above imo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Passing amountOut to getNextSqrtPriceFromOutput.

Comment thread src/libraries/SwapMath.sol
Comment thread src/libraries/SwapMath.sol Outdated
// cap the output amount to not exceed the remaining output amount
amountOut = uint256(amountRemaining);
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput(
sqrtPriceCurrentX96, liquidity, uint256(amountRemaining), zeroForOne

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

was gonna ask same thing - even if it doesn't make a difference I think the code reads better with using the variable we just defined above imo

@hensha256 hensha256 merged commit d50bfc5 into Uniswap:main May 26, 2024
@shuhuiluo shuhuiluo deleted the optimize-swapmath branch May 26, 2024 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants