FlapVaultEx
Overview
FlapVaultEx is an asynchronous, intent-based settlement layer that lets a vault swap any token for any other token without knowing anything about DEX routing. The vault submits an intent — "sell this much of inputToken, I want outputToken" — and Flap's off-chain executor sources the best route across DEX aggregators and settles it back on-chain.
It bridges on-chain custody with off-chain routing:
The contract custodies the input and records a
PENDINGrequest.The off-chain executor finds the best route and calls
fulfillSwapwith the route calldata.The contract measures the real output by balance delta, enforces an on-chain minimum, takes the fee, and delivers the result straight back to the vault via a callback — atomically.
There are no signatures and no routing logic in the vault. Routes are bound by on-chain whitelists, output is verified by balance delta, and the executor can only settle — never redirect — the funds.
Deployed addresses:
BNB Chain Mainnet
0x000048Df69f3d2af22Df6DC2d3B01FB592b90000
200,000
Primary use cases:
Converting tax/fee revenue held in an arbitrary token into a stablecoin or a target dividend asset
Consolidating multiple fee tokens into one
Rebalancing a vault's holdings without building or auditing DEX routing
How it works
Step by step:
The vault calls
requestSwap()with{inputToken, outputToken, inputAmount}, transferring the input ERC-20 into FlapVaultEx via approval.FlapVaultEx records the request as
PENDINGand emitsFlapVaultExRequested.The off-chain executor indexes the event, quotes across DEX aggregators, and picks the best route.
The executor calls
fulfillSwap()with the winning route (target,allowanceTarget,data) and aminOutputAmountfloor.FlapVaultEx approves exactly the input amount, performs the router call, resets the approval, and measures the received output by balance delta.
If the output is at least
minOutputAmount, it takes the fee and delivers the net output to the vault, then callsonFlapVaultExSettled(...). If anything fails, the whole call reverts.
Request lifecycle
NONE
0
No such request.
PENDING
1
Input custodied; awaiting settlement or a refund.
FULFILLED
2
Swapped; output delivered to the vault.
REFUNDED
3
Input returned to the vault (fee taken from the input side).
FAILED
4
A failed batch item whose automatic refund could not be delivered; retryable via refundSwap.
Fees
FlapVaultEx charges a small protocol fee, currently 0.5% (50 bps). The fee exists primarily to discourage abuse — creating requests only to immediately refund them for free, or spamming requests the executor has to process.
How the fee is taken:
It is charged on the quote-token side: if the input is a configured quote token (e.g. USDT/USDC/WBNB) and the output is not, it's taken from the input; if the output is the quote, from the output; if both or neither, it falls back to the admin-configured side.
The rate is snapshotted at
requestSwap. Settlement always uses the snapshotted rate, so a fee change never affects an in-flight request.A refund also takes the snapshotted
feeBps, always from the input side (a refund has no output) — so a request → refund round trip is not free.
Callback gas limit
Callbacks run with a bounded gas budget — 200,000 on the current deployment — so settlement can never be griefed by an expensive callback. Keep onFlapVaultExSettled cheap; it should record the result, not do heavy work. Read the live value via maxCallbackGas().
How to integrate
Step 1 — Import the interfaces
Step 2 — Implement IFlapVaultExConsumer
Your vault must implement onFlapVaultExSettled. It is called for both outcomes, so branch on status:
status == 2(FULFILLED): the output token was delivered, net of the fee.status == 3(REFUNDED): the input token was returned, net of the input-side fee.
In both cases the funds are already in your vault when the callback runs. Validate the caller, then do whatever your accounting needs.
Step 3 — Trigger a swap
Do not let the caller pick the tokens or amount. Fix the parameters at initialization (like the other vault examples in this repo) and expose a no-argument swap() that just swaps what the vault actually holds. Revenue (tax payouts, direct transfers) accumulates in the vault, so read the live balance — balanceOf(address(this)) — and only create a request once it is worth it.
Keep the returned requestId (or index it from FlapVaultExRequested) so you can correlate the callback.
Example vault
Notes:
Tokens and the swap threshold are fixed at initialization;
swap()takes no arguments and uses the livebalanceOf(address(this)), so a caller can never steer the vault into selling the wrong asset or amount.No-arg
swap()is safe to leave callable by anyone (it only touches the vault's own revenue) — restrict it with an owner/operator check if your design needs to.Approve
FlapVaultExfor exactly the amount you swap (it pulls exactly that; fee-on-transfer inputs are rejected).Branch on
status— the same callback fires whether the swap settled or the input was refunded — and gate it onmsg.sender == flapVaultExplus your own request-id tracking.
Native (BNB/ETH) input and output
FlapVaultEx only swaps ERC-20 tokens — native is disabled. So if your vault wants to accept or pay out native BNB, wrap it into WBNB yourself (and unwrap on the way out):
Native input (sell native): wrap the vault's native balance into WBNB with
deposit{value: amount}(), then request a swap from WBNB to your target token.Native output (buy native): request a swap from your token to WBNB, then unwrap the WBNB you receive in the callback back into BNB with
withdraw(amount).
Native input — sell native for an ERC-20
Vault receives native, wraps it, then requests WBNB → outputToken:
On the refund path the input comes back as WBNB; call IWETH(WBNB).withdraw(amount) if the vault wants native again.
Native output — buy native with an ERC-20
Vault requests inputToken → WBNB, then unwraps the delivered amount in the callback:
Reference
Full interface
Last updated