Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and optimize SwapMath #280

Merged
merged 23 commits into from
May 26, 2024

Conversation

shuhuiluo
Copy link
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
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
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.
sqrtPriceNextX96 = sqrtPriceTargetX96;
} else {
// cap the output amount to not exceed the remaining output amount
amountOut = uint256(amountRemaining);
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput(
sqrtPriceCurrentX96, liquidity, uint256(amountRemaining), zeroForOne
Copy link
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
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
Member

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
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.

src/libraries/SwapMath.sol Show resolved Hide resolved
sqrtPriceNextX96 = sqrtPriceTargetX96;
} else {
// cap the output amount to not exceed the remaining output amount
amountOut = uint256(amountRemaining);
sqrtPriceNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput(
sqrtPriceCurrentX96, liquidity, uint256(amountRemaining), zeroForOne
Copy link
Member

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
5 of 6 checks passed
@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