Skip to content

Instantly share code, notes, and snippets.

@patcito
Last active March 17, 2026 23:04
Show Gist options
  • Select an option

  • Save patcito/564ad01dc2797739a0289e43fb524183 to your computer and use it in GitHub Desktop.

Select an option

Save patcito/564ad01dc2797739a0289e43fb524183 to your computer and use it in GitHub Desktop.

Swap Collateral via LeverageRfqEngine — Infra / Multisig Guide

Overview

The swapCollateralFlashWithSession function on the LeverageRfqEngine contract allows users to swap one collateral type for another within the FlyingTulip lending protocol (PositionsManager). The executor service acts as the filler, routing swaps through DEX aggregators (Odos) via a flash callback pattern.

Architecture

User signs EIP-712 order → Executor API → Service picks up order
  → Odos DEX quote → Encode flash fill data
  → Call swapCollateralFlashWithSession on LeverageRfqEngine
  → Engine withdraws user's sellToken collateral
  → Engine sends sellToken to SwapFiller contract
  → SwapFiller.onFlash() executes DEX swap via Odos router
  → DEX sends buyToken directly to Engine (via Odos receiver param)
  → Engine deposits buyToken as new collateral for user

Scripts

All scripts are in ft-safe/scripts/0xddd/swap_filler/:

Script Purpose
deploy.py Deploy SwapFiller + configure SessionManager (ape-safe multisig)
create_swap_order.py Create and submit swap orders to executor API (standalone Python)

1. Deploy SwapFiller (ape-safe multisig)

What is SwapFiller?

A minimal helper contract implementing IFlash.onFlash(). It receives tokens from the LeverageRfqEngine during a flash collateral swap, executes DEX swaps via arbitrary routers using forceApprove (SafeERC20), resets allowances to 0 after each swap, and the DEX output goes directly to the engine.

Source

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

contract SwapFiller is IFlash {
    using SafeERC20 for IERC20;
    address public immutable leverageRfqEngine;

    constructor(address _leverageRfqEngine) {
        leverageRfqEngine = _leverageRfqEngine;
    }

    function onFlash(bytes calldata data) external {
        require(msg.sender == leverageRfqEngine);
        (
            address[] memory tokens,
            uint256[] memory amounts,
            address[] memory routers,
            bytes[] memory calls
        ) = abi.decode(data, (address[], uint256[], address[], bytes[]));

        for (uint256 i = 0; i < tokens.length; i++) {
            IERC20(tokens[i]).forceApprove(routers[i], amounts[i]);
            (bool ok, bytes memory ret) = routers[i].call(calls[i]);
            if (!ok) { assembly { revert(add(ret, 32), mload(ret)) } }
            IERC20(tokens[i]).forceApprove(routers[i], 0);  // revoke leftover
        }
    }
}

Deploy via 0xddd Safe

ft-safe/scripts/0xddd/swap_filler/deploy.py:

from ape import Contract, project
from ape_safe.cli import propose_from_simulation

@propose_from_simulation()
def cli(safe):
    """Deploy SwapFiller for LeverageRfqEngine collateral swaps."""
    assert safe == "0xddd8FBe1ddD4D6d99CB9851AFD4D47aC6EeD9fb8", "Wrong multisig"

    leverage_rfq_engine = Contract("0xddDfFBb0F870EF1b332019cFCAd8411aD35ebE2D")
    session_manager = Contract("0x52Ef449D44cC4205fa44bF644dEE15611FC30734")

    swap_filler = project.SwapFiller.deploy(leverage_rfq_engine, gas_price=0)
    assert swap_filler.leverageRfqEngine() == leverage_rfq_engine.address

    leverage_rfq_engine.setSessionManager(session_manager, gas_price=0)

    if not session_manager.allowedTarget(leverage_rfq_engine):
        session_manager.setAllowedTarget(leverage_rfq_engine, True, gas_price=0)
    assert session_manager.allowedTarget(leverage_rfq_engine)

Run:

ape safe propose scripts/0xddd/swap_filler/deploy.py --safe 0xddd --network sonic

Uses 0xddd (fdev) because pm.admin() = 0xddd8FBe1ddD4D6d99CB9851AFD4D47aC6EeD9fb8.

After deployment, set the SwapFiller address in the executor config:

leverageRfqEngine:
  swapFillerAddress: "0x<deployed_address>"

2. Create Swap Orders

ft-safe/scripts/0xddd/swap_filler/create_swap_order.py:

CLI usage

python scripts/0xddd/swap_filler/create_swap_order.py \
    --executor-url https://executor.flyingtulip.com \
    --delegate-key 0x... \
    --session-id 0x... \
    --sell-token 0x29219dd400f2Bf60E5a23d13Be72B486D4038894 \
    --buy-token 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38 \
    --sell-amount 50000000 \
    --buy-amount 100000000000000 \
    --user 0x... \
    --poll

Programmatic usage

from scripts.0xddd.swap_filler.create_swap_order import submit_swap_order, poll_order

result = submit_swap_order(
    executor_url="https://executor.flyingtulip.com",
    delegate_key="0x...",
    session_id="0x...",
    sell_token="0x29219dd400f2Bf60E5a23d13Be72B486D4038894",  # USDC
    buy_token="0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38",   # wS
    sell_amount=50_000_000,          # 50 USDC (6 decimals)
    buy_amount=100_000_000_000_000,  # 0.0001 wS min (18 decimals)
    user="0x...",
)

if result.get("success"):
    order = poll_order("https://executor.flyingtulip.com", result["orderId"])

The script:

  1. Fetches executor config (executor address)
  2. Reads session nonce from chain
  3. Computes SwapCollateralSession dataHash
  4. Signs EIP-712 SessionCall with the delegate key
  5. POSTs to the executor API
  6. Optionally polls until filled/failed

Dependencies

pip install web3 eth-account eth-abi requests

Prerequisites

  1. SwapFiller deployed via deploy.py
  2. Session: User must have a session on the SessionManager with the delegate key
  3. Collateral: User must have sellToken deposited in PositionsManager
  4. Engine debit: User must call pm.approveEngine(leverageRfqEngine, sellToken, amount)
  5. Allowed target: LeverageRfqEngine set as allowed target on SessionManager
  6. SessionManager on engine: engine.setSessionManager(sessionManagerAddr) called by pm.admin()

Contract Addresses (Sonic — Chain 146)

Contract Address
LeverageRfqEngine 0xddDfFBb0F870EF1b332019cFCAd8411aD35ebE2D
SessionManager 0x52Ef449D44cC4205fa44bF644dEE15611FC30734
PositionsManager 0x4E59D80de66cA3979A59E1b2BC61Ee69B9f95577
MetaSessionActions 0xC97b07B47a84f452ADa19f73A7f3a8174aC28218
USDC 0x29219dd400f2Bf60E5a23d13Be72B486D4038894
wS 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38
SwapFiller Deploy via scripts/0xddd/swap_filler/deploy.py

Executor Config

leverageRfqEngine:
  contractAddress: "0xddDfFBb0F870EF1b332019cFCAd8411aD35ebE2D"
  sessionManagerAddress: "0x52Ef449D44cC4205fa44bF644dEE15611FC30734"
  chainId: 146
  swapFillerAddress: "0x<your_deployed_swap_filler>"

dexRouter:
  odos:
    baseUrl: "https://api.odos.xyz"
    version: "v2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment