Robinhood MainnetChain ID: 4663
Contract detail

SwapperMultiStep

SwapperMultiStep is a verified Robinhood Chain smart contract at 0xcf3dfd18…0a231c66 with 0 indexed transactions.

Address type
Smart contract
Transactions
0
History coverage
No indexed activity found
Indexed token transfers
667
ERC-20 transfers
2210
Emitted events
316
Verification
Verified
Compiler
v0.8.33+commit.64118f21
Verification match
full
Creation transaction
0x932b9415…0c92e63b
Created at block
#79045
Language
solidity
EVM version
prague
License
none
Compiler optimization
Enabled · 100 runs
Verified contract

Contract source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {ISwapper} from "./interfaces/ISwapper.sol";

/// @title ISwapperMultiStep
/// @notice Interface for multi-step swappers that accept calldata for flexible routing
interface ISwapperMultiStep {
    /// @notice Sells `amountIn` of `input` tokens for `output` tokens via multiple steps.
    /// @param input The address of the input token.
    /// @param output The address of the output token.
    /// @param amountIn The amount of input tokens to sell.
    /// @param data Encoded swap steps (see SwapperMultiStep for format).
    function sell(IERC20 input, IERC20 output, uint256 amountIn, bytes calldata data) external;
}

/// @title SwapperMultiStep
/// @notice A flexible swap adapter that chains multiple swap steps, supporting both
///         DEX aggregators (like 1inch) and hardcoded swapper routes (like Pendle).
/// @dev The `data` parameter encodes an array of SwapStep structs that define the swap route.
///
/// Example flow: USDC -> sUSDe (via 1inch) -> PT-sUSDe (via Pendle swapper)
///
/// Data encoding (Python):
/// ```python
/// steps = [
///     (0, "0x1inch_router", "0xUSDC", "0xsUSDe", 68, calldata_from_1inch),
///     (1, "0xSwapper", "0xsUSDe", "0xPT_sUSDe", 2**256-1, b""),
/// ]
/// data = eth_abi.encode(['(uint8,address,address,address,uint256,bytes)[]'], [steps])
/// ```
contract SwapperMultiStep is ISwapperMultiStep {
    using SafeERC20 for IERC20;

    /// @notice Step type: external call to a DEX aggregator
    uint8 public constant STEP_TYPE_EXTERNAL = 0;

    /// @notice Step type: call to a hardcoded ISwapper
    uint8 public constant STEP_TYPE_SWAPPER = 1;

    /// @notice Magic value indicating no amount injection should be performed
    uint256 public constant NO_AMOUNT_INJECTION = type(uint256).max;

    /// @notice Represents a single swap step in a multi-hop route
    /// @param stepType Either STEP_TYPE_EXTERNAL (0) or STEP_TYPE_SWAPPER (1)
    /// @param target The contract to call (DEX router or ISwapper)
    /// @param tokenIn The input token for this step
    /// @param tokenOut The output token for this step
    /// @param amountOffset For external calls: byte offset in callData where to write the amount.
    ///                     Use NO_AMOUNT_INJECTION to skip injection.
    /// @param callData For external calls: the raw calldata to send. Ignored for swapper steps.
    struct SwapStep {
        uint8 stepType;
        address target;
        IERC20 tokenIn;
        IERC20 tokenOut;
        uint256 amountOffset;
        bytes callData;
    }

    /// @notice Emitted when a multi-step swap is executed
    event MultiSwapExecuted(
        IERC20 indexed input,
        IERC20 indexed output,
        uint256 amountIn,
        uint256 amountOut,
        uint256 numSteps
    );

    /// @notice Emitted when an individual step is executed
    event StepExecuted(
        uint256 indexed stepIndex,
        uint8 stepType,
        address target,
        IERC20 tokenIn,
        IERC20 tokenOut,
        uint256 amountIn,
        uint256 amountOut
    );

    error InvalidStepType(uint8 stepType);
    error ExternalCallFailed(address target, bytes returnData);
    error NoStepsProvided();
    error InsufficientBalance(IERC20 token, uint256 required, uint256 available);
    error AmountOffsetOutOfBounds(uint256 offset, uint256 dataLength);
    error TokenNotFullyConsumed(uint256 stepIndex, IERC20 token, uint256 remaining);

    /// @inheritdoc ISwapperMultiStep
    function sell(IERC20 input, IERC20 output, uint256 amountIn, bytes calldata data) external override {
        // Pull tokens from sender
        input.safeTransferFrom(msg.sender, address(this), amountIn);

        // Decode swap steps
        SwapStep[] memory steps = abi.decode(data, (SwapStep[]));
        if (steps.length == 0) revert NoStepsProvided();

        // Execute each step
        for (uint256 i = 0; i < steps.length; i++) {
            _executeStep(steps[i], i);
        }

        // Transfer output to sender
        uint256 amountOut = output.balanceOf(address(this));
        output.safeTransfer(msg.sender, amountOut);

        emit MultiSwapExecuted(input, output, amountIn, amountOut, steps.length);
    }

    /// @notice Executes a single swap step
    /// @param step The step to execute
    /// @param stepIndex The index of this step (for events)
    function _executeStep(SwapStep memory step, uint256 stepIndex) internal {
        uint256 balanceIn = step.tokenIn.balanceOf(address(this));
        if (balanceIn == 0) {
            revert InsufficientBalance(step.tokenIn, 1, 0);
        }

        uint256 balanceOutBefore = step.tokenOut.balanceOf(address(this));

        if (step.stepType == STEP_TYPE_EXTERNAL) {
            _executeExternalCall(step, balanceIn);
        } else if (step.stepType == STEP_TYPE_SWAPPER) {
            _executeSwapperCall(step, balanceIn);
        } else {
            revert InvalidStepType(step.stepType);
        }

        // Ensure tokenIn was fully consumed
        uint256 remainingIn = step.tokenIn.balanceOf(address(this));
        if (remainingIn > 0) {
            revert TokenNotFullyConsumed(stepIndex, step.tokenIn, remainingIn);
        }

        uint256 balanceOutAfter = step.tokenOut.balanceOf(address(this));
        uint256 amountOut = balanceOutAfter - balanceOutBefore;

        emit StepExecuted(
            stepIndex,
            step.stepType,
            step.target,
            step.tokenIn,
            step.tokenOut,
            balanceIn,
            amountOut
        );
    }

    /// @notice Executes an external call to a DEX aggregator
    /// @param step The step containing target and calldata
    /// @param amountIn The amount of input tokens to swap
    function _executeExternalCall(SwapStep memory step, uint256 amountIn) internal {
        bytes memory callData = step.callData;
        uint256 amountOffset = step.amountOffset;

        // Inject amount at specified offset if requested
        if (amountOffset != NO_AMOUNT_INJECTION) {
            if (amountOffset + 32 > callData.length) {
                revert AmountOffsetOutOfBounds(amountOffset, callData.length);
            }
            assembly {
                // callData in memory: [length (32 bytes)][data...]
                // Add 32 to skip length, then add offset to reach the amount position
                mstore(add(add(callData, 32), amountOffset), amountIn)
            }
        }

        // Approve target for the input amount
        step.tokenIn.forceApprove(step.target, amountIn);

        // Execute the call
        (bool success, bytes memory returnData) = step.target.call(callData);

        // Reset approval to 0
        step.tokenIn.forceApprove(step.target, 0);

        if (!success) {
            revert ExternalCallFailed(step.target, returnData);
        }
    }

    /// @notice Executes a call to a hardcoded ISwapper
    /// @param step The step containing swapper address and tokens
    /// @param amountIn The amount of input tokens to swap
    function _executeSwapperCall(SwapStep memory step, uint256 amountIn) internal {
        // Approve swapper for the input amount
        step.tokenIn.forceApprove(step.target, amountIn);

        // Call the swapper
        ISwapper(step.target).sell(step.tokenIn, step.tokenOut, amountIn);

        // Reset approval to 0
        step.tokenIn.forceApprove(step.target, 0);
    }
}