From f3404ccce0cc2613732af95a1fcfa02ee25590d9 Mon Sep 17 00:00:00 2001 From: ChiTimesChi <88190723+ChiTimesChi@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:09:46 +0000 Subject: [PATCH] feat: Synapse Intent Previewer --- .../legacy/router/interfaces/IWETH9.sol | 2 +- .../router/SynapseIntentPreviewer.sol | 300 +++++++++++++++++- 2 files changed, 299 insertions(+), 3 deletions(-) diff --git a/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol b/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol index a451ff5134..2eab02750f 100644 --- a/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol +++ b/packages/contracts-rfq/contracts/legacy/router/interfaces/IWETH9.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.17; interface IWETH9 { function deposit() external payable; diff --git a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol index aeef811abe..caec160b36 100644 --- a/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol +++ b/packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol @@ -3,9 +3,30 @@ pragma solidity 0.8.24; // ════════════════════════════════════════════════ INTERFACES ═════════════════════════════════════════════════════ +import {ISynapseIntentPreviewer} from "../interfaces/ISynapseIntentPreviewer.sol"; import {ISynapseIntentRouter} from "../interfaces/ISynapseIntentRouter.sol"; +import {ISwapQuoter} from "../legacy/rfq/interfaces/ISwapQuoter.sol"; +import {IDefaultExtendedPool, IDefaultPool} from "../legacy/router/interfaces/IDefaultExtendedPool.sol"; +import {IWETH9} from "../legacy/router/interfaces/IWETH9.sol"; -contract SynapseIntentPreviewer { +// ═════════════════════════════════════════════ INTERNAL IMPORTS ══════════════════════════════════════════════════ + +import {Action, DefaultParams, LimitedToken, SwapQuery} from "../legacy/router/libs/Structs.sol"; +import {ZapDataV1} from "../libs/ZapDataV1.sol"; + +contract SynapseIntentPreviewer is ISynapseIntentPreviewer { + /// @notice The address reserved for the native gas token (ETH on Ethereum and most L2s, AVAX on Avalanche, etc.). + address public constant NATIVE_GAS_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /// @dev Amount value that signals that the Zap step should be performed using the full ZapRecipient balance. + uint256 internal constant FULL_BALANCE = type(uint256).max; + + error SIP__PoolTokenMismatch(); + error SIP__PoolZeroAddress(); + error SIP__RawParamsEmpty(); + error SIP__TokenNotNative(); + + /// @inheritdoc ISynapseIntentPreviewer function previewIntent( address swapQuoter, address tokenIn, @@ -16,6 +37,281 @@ contract SynapseIntentPreviewer { view returns (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps) { - // TODO: implement + // First, check if the intent is a no-op. + if (tokenIn == tokenOut) { + return (amountIn, new ISynapseIntentRouter.StepParams[](0)); + } + + // Obtain the swap quote, don't put any restrictions on the actions allowed to complete the intent. + SwapQuery memory query = ISwapQuoter(swapQuoter).getAmountOut( + LimitedToken({token: tokenIn, actionMask: type(uint256).max}), tokenOut, amountIn + ); + + // Check if a quote was returned. + amountOut = query.minAmountOut; + if (amountOut == 0) { + return (0, new ISynapseIntentRouter.StepParams[](0)); + } + + // At this point we have a quote for a non-trivial action, therefore `query.rawParams` is not empty. + if (query.rawParams.length == 0) revert SIP__RawParamsEmpty(); + DefaultParams memory params = abi.decode(query.rawParams, (DefaultParams)); + + // Create the steps for the intent based on the action type. + if (params.action == Action.Swap) { + steps = _createSwapSteps(tokenIn, tokenOut, amountIn, params); + } else if (params.action == Action.AddLiquidity) { + steps = _createAddLiquiditySteps(tokenIn, tokenOut, params); + } else if (params.action == Action.RemoveLiquidity) { + steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params); + } else { + steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn); + } + } + + /// @notice Helper function to create steps for a swap. + function _createSwapSteps( + address tokenIn, + address tokenOut, + uint256 amountIn, + DefaultParams memory params + ) + internal + view + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + address pool = params.pool; + if (pool == address(0)) revert SIP__PoolZeroAddress(); + // Default Pools can only host wrapped native tokens. + // Check if we start from the native gas token. + if (tokenIn == NATIVE_GAS_TOKEN) { + // Get the address of the wrapped native token. + address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexFrom); + // Sanity check tokenOut vs tokenIndexTo. + if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); + // Native => WrappedNative + WrappedNative => TokenOut. + return _toStepsArray( + _createWrapNativeStep({wrappedNative: wrappedNative, amountIn: amountIn}), + _createSwapStep({tokenIn: wrappedNative, params: params}) + ); + } + + // Sanity check tokenIn vs tokenIndexFrom. + if (IDefaultPool(pool).getToken(params.tokenIndexFrom) != tokenIn) revert SIP__PoolTokenMismatch(); + + // Check if we end with the native gas token. + if (tokenOut == NATIVE_GAS_TOKEN) { + // Get the address of the wrapped native token. + address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexTo); + // TokenIn => WrappedNative + WrappedNative => Native. + return _toStepsArray( + _createSwapStep({tokenIn: tokenIn, params: params}), + _createUnwrapNativeStep({wrappedNative: wrappedNative}) + ); + } + + // Sanity check tokenOut vs tokenIndexTo. + if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); + + // TokenIn => TokenOut. + return _toStepsArray(_createSwapStep({tokenIn: tokenIn, params: params})); + } + + /// @notice Helper function to create steps for adding liquidity. + function _createAddLiquiditySteps( + address tokenIn, + address tokenOut, + DefaultParams memory params + ) + internal + view + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + address pool = params.pool; + if (pool == address(0)) revert SIP__PoolZeroAddress(); + // Sanity check tokenIn vs tokenIndexFrom. + if (IDefaultPool(pool).getToken(params.tokenIndexFrom) != tokenIn) revert SIP__PoolTokenMismatch(); + // Sanity check tokenOut vs pool's LP token. + _verifyLpToken(pool, tokenOut); + // Figure out how many tokens does the pool support. + uint256[] memory amounts; + for (uint8 i = 0;; i++) { + // solhint-disable-next-line no-empty-blocks + try IDefaultExtendedPool(pool).getToken(i) returns (address) { + // Token exists, continue. + } catch { + // No more tokens, allocate the array using the correct size. + amounts = new uint256[](i); + break; + } + } + return _toStepsArray( + ISynapseIntentRouter.StepParams({ + token: tokenIn, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: pool, + // addLiquidity(amounts, minToMint, deadline) + payload_: abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max)), + // amountIn is encoded within `amounts` at `TOKEN_IN_INDEX`, `amounts` is encoded after + // (amounts.offset, minToMint, deadline, amounts.length). + amountPosition_: 4 + 32 * 4 + 32 * uint16(params.tokenIndexFrom) + }) + }) + ); + } + + /// @notice Helper function to create steps for removing liquidity. + function _createRemoveLiquiditySteps( + address tokenIn, + address tokenOut, + DefaultParams memory params + ) + internal + view + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + address pool = params.pool; + if (pool == address(0)) revert SIP__PoolZeroAddress(); + // Sanity check tokenIn vs pool's LP token. + _verifyLpToken(pool, tokenIn); + // Sanity check tokenOut vs tokenIndexTo. + if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch(); + return _toStepsArray( + ISynapseIntentRouter.StepParams({ + token: tokenIn, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: pool, + // removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline) + payload_: abi.encodeCall( + IDefaultExtendedPool.removeLiquidityOneToken, (0, params.tokenIndexTo, 0, type(uint256).max) + ), + // amountIn is encoded as the first parameter: tokenAmount + amountPosition_: 4 + }) + }) + ); + } + + function _verifyLpToken(address pool, address token) internal view { + (,,,,,, address lpToken) = IDefaultExtendedPool(pool).swapStorage(); + if (lpToken != token) revert SIP__PoolTokenMismatch(); + } + + /// @notice Helper function to create steps for wrapping or unwrapping native gas tokens. + function _createHandleHativeSteps( + address tokenIn, + address tokenOut, + uint256 amountIn + ) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory steps) + { + if (tokenIn == NATIVE_GAS_TOKEN) { + // tokenOut is Wrapped Native + return _toStepsArray(_createWrapNativeStep({wrappedNative: tokenOut, amountIn: amountIn})); + } + // Sanity check tokenOut + if (tokenOut != NATIVE_GAS_TOKEN) revert SIP__TokenNotNative(); + // tokenIn is Wrapped Native + return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn})); + } + + /// @notice Helper function to create a single step for a swap. + function _createSwapStep( + address tokenIn, + DefaultParams memory params + ) + internal + pure + returns (ISynapseIntentRouter.StepParams memory) + { + return ISynapseIntentRouter.StepParams({ + token: tokenIn, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: params.pool, + // swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline) + payload_: abi.encodeCall( + IDefaultPool.swap, (params.tokenIndexFrom, params.tokenIndexTo, 0, 0, type(uint256).max) + ), + // amountIn is encoded as the third parameter: `dx` + amountPosition_: 4 + 32 * 2 + }) + }); + } + + /// @notice Helper function to create a single step for wrapping native gas tokens. + function _createWrapNativeStep( + address wrappedNative, + uint256 amountIn + ) + internal + pure + returns (ISynapseIntentRouter.StepParams memory) + { + return ISynapseIntentRouter.StepParams({ + token: NATIVE_GAS_TOKEN, + amount: FULL_BALANCE, + msgValue: amountIn, + zapData: ZapDataV1.encodeV1({ + target_: wrappedNative, + // deposit() + payload_: abi.encodeCall(IWETH9.deposit, ()), + // amountIn is not encoded + amountPosition_: ZapDataV1.AMOUNT_NOT_PRESENT + }) + }); + } + + /// @notice Helper function to create a single step for unwrapping native gas tokens. + function _createUnwrapNativeStep(address wrappedNative) + internal + pure + returns (ISynapseIntentRouter.StepParams memory) + { + return ISynapseIntentRouter.StepParams({ + token: wrappedNative, + amount: FULL_BALANCE, + msgValue: 0, + zapData: ZapDataV1.encodeV1({ + target_: wrappedNative, + // withdraw(amount) + payload_: abi.encodeCall(IWETH9.withdraw, (0)), + // amountIn encoded as the first parameter + amountPosition_: 4 + }) + }); + } + + /// @notice Helper function to construct an array of steps having a single step. + function _toStepsArray(ISynapseIntentRouter.StepParams memory step0) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory) + { + ISynapseIntentRouter.StepParams[] memory steps = new ISynapseIntentRouter.StepParams[](1); + steps[0] = step0; + return steps; + } + + /// @notice Helper function to construct an array of steps having two steps. + function _toStepsArray( + ISynapseIntentRouter.StepParams memory step0, + ISynapseIntentRouter.StepParams memory step1 + ) + internal + pure + returns (ISynapseIntentRouter.StepParams[] memory) + { + ISynapseIntentRouter.StepParams[] memory steps = new ISynapseIntentRouter.StepParams[](2); + steps[0] = step0; + steps[1] = step1; + return steps; } }