synapsecns/sanguine

View on GitHub
packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
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";

// ═════════════════════════════════════════════ 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__NoOpForwardNotSupported();
    error SIP__PoolTokenMismatch();
    error SIP__PoolZeroAddress();
    error SIP__RawParamsEmpty();
    error SIP__TokenNotNative();

    /// @inheritdoc ISynapseIntentPreviewer
    // solhint-disable-next-line code-complexity
    function previewIntent(
        address swapQuoter,
        address forwardTo,
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    )
        external
        view
        returns (uint256 amountOut, ISynapseIntentRouter.StepParams[] memory steps)
    {
        // First, check if the intent is a no-op.
        if (tokenIn == tokenOut) {
            if (forwardTo != address(0)) revert SIP__NoOpForwardNotSupported();
            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, forwardTo);
        } else if (params.action == Action.AddLiquidity) {
            steps = _createAddLiquiditySteps(tokenIn, tokenOut, params, forwardTo);
        } else if (params.action == Action.RemoveLiquidity) {
            steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params, forwardTo);
        } else {
            steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn, forwardTo);
        }
    }

    /// @notice Helper function to create steps for a swap.
    function _createSwapSteps(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        DefaultParams memory params,
        address forwardTo
    )
        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. Forwarding is done in the second step.
            return _toStepsArray(
                _createWrapNativeStep({wrappedNative: wrappedNative, msgValue: amountIn, forwardTo: address(0)}),
                _createSwapStep({tokenIn: wrappedNative, tokenOut: tokenOut, params: params, forwardTo: forwardTo})
            );
        }

        // 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. Forwarding is done in the second step.
            return _toStepsArray(
                _createSwapStep({tokenIn: tokenIn, tokenOut: wrappedNative, params: params, forwardTo: address(0)}),
                _createUnwrapNativeStep({wrappedNative: wrappedNative, forwardTo: forwardTo})
            );
        }

        // Sanity check tokenOut vs tokenIndexTo.
        if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch();

        // TokenIn => TokenOut.
        ISynapseIntentRouter.StepParams memory step =
            _createSwapStep({tokenIn: tokenIn, tokenOut: tokenOut, params: params, forwardTo: forwardTo});
        return _toStepsArray(step);
    }

    /// @notice Helper function to create steps for adding liquidity.
    function _createAddLiquiditySteps(
        address tokenIn,
        address tokenOut,
        DefaultParams memory params,
        address forwardTo
    )
        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,
                    finalToken_: tokenOut,
                    forwardTo_: forwardTo,
                    // 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,
        address forwardTo
    )
        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,
                    finalToken_: tokenOut,
                    forwardTo_: forwardTo,
                    // 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,
        address forwardTo
    )
        internal
        pure
        returns (ISynapseIntentRouter.StepParams[] memory steps)
    {
        if (tokenIn == NATIVE_GAS_TOKEN) {
            // tokenOut is Wrapped Native
            return _toStepsArray(
                _createWrapNativeStep({wrappedNative: tokenOut, msgValue: amountIn, forwardTo: forwardTo})
            );
        }
        // Sanity check tokenOut
        if (tokenOut != NATIVE_GAS_TOKEN) revert SIP__TokenNotNative();
        // tokenIn is Wrapped Native
        return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn, forwardTo: forwardTo}));
    }

    /// @notice Helper function to create a single step for a swap.
    function _createSwapStep(
        address tokenIn,
        address tokenOut,
        DefaultParams memory params,
        address forwardTo
    )
        internal
        pure
        returns (ISynapseIntentRouter.StepParams memory)
    {
        return ISynapseIntentRouter.StepParams({
            token: tokenIn,
            amount: FULL_BALANCE,
            msgValue: 0,
            zapData: ZapDataV1.encodeV1({
                target_: params.pool,
                finalToken_: tokenOut,
                forwardTo_: forwardTo,
                // 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 msgValue,
        address forwardTo
    )
        internal
        pure
        returns (ISynapseIntentRouter.StepParams memory)
    {
        return ISynapseIntentRouter.StepParams({
            token: NATIVE_GAS_TOKEN,
            amount: FULL_BALANCE,
            msgValue: msgValue,
            zapData: ZapDataV1.encodeV1({
                target_: wrappedNative,
                finalToken_: wrappedNative,
                forwardTo_: forwardTo,
                // 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,
        address forwardTo
    )
        internal
        pure
        returns (ISynapseIntentRouter.StepParams memory)
    {
        return ISynapseIntentRouter.StepParams({
            token: wrappedNative,
            amount: FULL_BALANCE,
            msgValue: 0,
            zapData: ZapDataV1.encodeV1({
                target_: wrappedNative,
                finalToken_: NATIVE_GAS_TOKEN,
                forwardTo_: forwardTo,
                // 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;
    }
}