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.
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
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) |
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.
// 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
}
}
}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 sonicUses 0xddd (fdev) because pm.admin() = 0xddd8FBe1ddD4D6d99CB9851AFD4D47aC6EeD9fb8.
After deployment, set the SwapFiller address in the executor config:
leverageRfqEngine:
swapFillerAddress: "0x<deployed_address>"ft-safe/scripts/0xddd/swap_filler/create_swap_order.py:
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... \
--pollfrom 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:
- Fetches executor config (executor address)
- Reads session nonce from chain
- Computes
SwapCollateralSessiondataHash - Signs EIP-712
SessionCallwith the delegate key - POSTs to the executor API
- Optionally polls until filled/failed
pip install web3 eth-account eth-abi requests- SwapFiller deployed via
deploy.py - Session: User must have a session on the SessionManager with the delegate key
- Collateral: User must have sellToken deposited in PositionsManager
- Engine debit: User must call
pm.approveEngine(leverageRfqEngine, sellToken, amount) - Allowed target: LeverageRfqEngine set as allowed target on SessionManager
- SessionManager on engine:
engine.setSessionManager(sessionManagerAddr)called by pm.admin()
| Contract | Address |
|---|---|
| LeverageRfqEngine | 0xddDfFBb0F870EF1b332019cFCAd8411aD35ebE2D |
| SessionManager | 0x52Ef449D44cC4205fa44bF644dEE15611FC30734 |
| PositionsManager | 0x4E59D80de66cA3979A59E1b2BC61Ee69B9f95577 |
| MetaSessionActions | 0xC97b07B47a84f452ADa19f73A7f3a8174aC28218 |
| USDC | 0x29219dd400f2Bf60E5a23d13Be72B486D4038894 |
| wS | 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38 |
| SwapFiller | Deploy via scripts/0xddd/swap_filler/deploy.py |
leverageRfqEngine:
contractAddress: "0xddDfFBb0F870EF1b332019cFCAd8411aD35ebE2D"
sessionManagerAddress: "0x52Ef449D44cC4205fa44bF644dEE15611FC30734"
chainId: 146
swapFillerAddress: "0x<your_deployed_swap_filler>"
dexRouter:
odos:
baseUrl: "https://api.odos.xyz"
version: "v2"