If you'd rather your Solidity strategy submit orders fully on-chain
instead of POSTing to our HTTP API, just call
engine.broadcastOrder(order). Our executor watches the engine's
OrderBroadcast event on Sonic mainnet and fills the order for you
within seconds.
This is permissionless: the engine has no caller check — security
is the user's EIP-712 signature (or, for broadcastOrder specifically,
the engine itself records preSignature[msg.sender, digest] = true and
later short-circuits _isValidSig on that). You don't need any
allowlist from us. Just have your strategy contract call
broadcastOrder and we settle.
End-to-end smoke on dev 2026-05-26: user broadcast tx
0x64e27d7d
filled by our executor in 9 seconds
(fill tx 0x52be19e5).
Same code is live on prod.
| Dev | Prod | |
|---|---|---|
| LeverageRfqEngine | 0x8f143D84Ebf0751E56437A62BAB0528d1c8657BF |
0x8263a07504d93cB95e0a74f3627bb15faaf140e2 |
| ftUSD | 0x04E6227d51Fc10AFb6c270017690a2B0a1d4427B |
0xF7D85EC4E7710f71992752eac2111312e73E9C9C |
| USDC | 0x29219dd400f2Bf60E5a23d13Be72B486D4038894 (chain-wide) |
|
| wS | 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38 (chain-wide) |
|
| stS | 0xE5DA20F15420aD15DE0fa650600aFc998bbE3955 (chain-wide) |
|
| Our executor relayer (FYI) | 0x794EB780F0F50591f376ea1f1492e0FD5d234d46 |
0xa505815A526f1200c17B7ffaE0067318d734b9d8 |
Chain: Sonic mainnet (chainId 146).
interface ILeverageRfqEngine {
struct LeveragedOrder {
uint8 action; // 0 OPEN | 1 CLOSE | 2 SWAP
address user; // MUST equal msg.sender (engine enforces)
address sellToken;
address buyToken;
uint256 sellAmount;
uint256 buyAmount; // min-out (slippage floor) — SET A REAL VALUE
uint32 validTo;
uint256 feeAmount;
}
function broadcastOrder(LeveragedOrder calldata order) external returns (bytes32 digest);
function cancelOrder(LeveragedOrder calldata order) external;
}
contract MyStrategy {
ILeverageRfqEngine constant ENGINE = ILeverageRfqEngine(0x8263a07504d93cB95e0a74f3627bb15faaf140e2);
address constant FTUSD = 0xF7D85EC4E7710f71992752eac2111312e73E9C9C;
address constant STS = 0xE5DA20F15420aD15DE0fa650600afc998bbE3955;
// Open a collateral swap (0.1 ftUSD -> stS, valid 1h)
function swapFtusdToStS() external {
ILeverageRfqEngine.LeveragedOrder memory o = ILeverageRfqEngine.LeveragedOrder({
action: 2, // SWAP
user: address(this), // engine requires user == msg.sender
sellToken: FTUSD,
buyToken: STS,
sellAmount: 100_000, // 0.1 ftUSD (6dp)
buyAmount: 1, // min stS out — SET A REAL FLOOR
validTo: uint32(block.timestamp + 3600),
feeAmount: 0
});
bytes32 digest = ENGINE.broadcastOrder(o);
// digest is the order's canonical ID; you can store it to track
// / cancel later via ENGINE.cancelOrder(o).
}
}Prereqs your strategy must have done before its first broadcast:
- Approve borrow on PositionsManager (only for
action=0OPEN):IPositionsManager(PM).approveBorrow(ENGINE, sellToken);
- Approve token to engine for any sell token you'll trade:
IERC20(sellToken).approve(ENGINE, type(uint256).max);
- Have collateral on PositionsManager for
action=1CLOSE oraction=2SWAP (the engine pulls from your PM position, swaps, deposits the proceeds back).
Once those are set up once, you can broadcastOrder as many times as you want.
pip install web3 eth-account
USER_PK=0x<key> python broadcast_order.pySee broadcast_order.py in this gist. It's the EOA equivalent —
useful for testing or human-triggered orders.
- Engine emits
OrderBroadcast(owner, digest, order)and setspreSignature[owner, digest] = true. - Our executor's chain watcher (polling every 3s) picks up the event.
- Executor resolves the DEX route (Odos / KyberSwap / our composite
ftUSD ↔ Xvia MintAndRedeem) and submits the fill via our SwapFiller. - Engine's
_isValidSigshort-circuits onpreSignature[user, digest], so the executor callsopenLeverageFlash/swapCollateralFlash/closeLeverageFlashwithsig=""and the engine accepts it. - Within ~5–10 seconds of your broadcast tx confirming, the fill tx lands and your position updates on PositionsManager.
If something rejects the fill (insufficient collateral, sim revert,
expired order), the order is parked for retry until validTo — our
executor doesn't move funds it shouldn't.
ENGINE.cancelOrder(sameOrderStructAsBroadcast);Only order.user == msg.sender can cancel. The engine sets
filledDigests[digest] = true to permanently prevent the fill.
Still supported and slightly faster (sub-second from our side): see the HTTP gist. Both paths can coexist; we de-dupe by order digest.