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);
}
}