Execute A Swap
Your contract calls swap on the pool. The pool debits the input token from your contract, and
delivers the output token to the destination you name.
Prerequisites
- You have worked through Check eligibility — every read there is free, and a swap that fails one of them costs gas for nothing
- You have a fresh quote from Preview a swap
The function
function swap(
address fromToken,
address toToken,
uint256 amount,
address destination,
uint256 minAmountOut
) external;
Two shorter overloads exist — one without minAmountOut, one without either destination or
minAmountOut. Use the five-argument form. Omitting minAmountOut means accepting whatever the
pool delivers, and the whole purpose of quoting is to be able to refuse a worse price.
amount is in the smallest unit of fromToken — 6 decimals for a stablecoin, 18 for iBENJI.
Step 1: Approve the pool
The pool moves your input token with transferFrom, so your contract must approve it first. The pool
never takes tokens you have not approved.
Approve exactly the amount you are about to swap, in the same transaction as the swap.
IERC20(fromToken).approve(pool, amount);
ISwapPool(pool).swap(fromToken, toToken, amount, destination, minAmountOut);
This is deliberately narrow. A single unlimited approval — approve(pool, type(uint256).max) — is
simpler to write, but leaves your whole balance spendable by the pool between swaps. Approving per
swap costs nothing extra and leaves no standing allowance.
Step 2: Choose the destination
The rule depends on direction, and it is enforced by the contract rather than by any interface.
| Direction | Destination |
|---|---|
| iBENJI → stablecoin | use your calling contract. Naming another address is refused with STABLECOIN_MUST_SELF_DELIVER |
| stablecoin → iBENJI | may be a different address, provided it is authorized on that pair |
Delivering to your own contract is always permitted in either direction, so a contract that always self-delivers never encounters the rule at all. That is the safe default and the one we recommend.
Self-delivery of stablecoin output is required in production, and nothing on either list changes that — not an authorization we grant, and not an entry on your contract's own allowlist. The check runs ahead of both. So a design that depends on sending stablecoin straight to a third party will not work; deliver to your contract and transfer onward yourself.
Still validate the recipient in your own contract. The refusal protects the stablecoin direction only, it is pool configuration rather than a property of the interface, and stablecoin cannot be recovered once delivered — so treat it as a backstop rather than your only check.
You can check whether a destination is authorized with isTraderAllowed before you swap — see
Check eligibility. Naming a destination other than your calling
contract requires a per-destination approval in addition to that authorization. Without one the swap is
refused.
Confirm this with your Franklin Templeton contact before designing around a third-party destination.
Step 3: Set minAmountOut
Pass the net figure from a fresh quote. If conditions change between quoting and settling so
that you would receive less, the swap reverts with
refused rather than settling on worse
terms.
Re-quote immediately before executing. Fees can be changed by the pool owner and take effect immediately, and the reserve moves as other participants swap.
A floor of 0 is not "no floor" if your contract re-quotes.
- Passing
0looks like "accept anything", and against the swap contract directly that is what it means. - But if your own contract responds to a
0by fetching a quote and using that as the floor — a natural thing to write, and what the template does — the floor is derived from the same calculation, with the same inputs, in the same transaction as the swap. It always equals what is delivered, so it can never bind, which means it offers no protection even though it looks like it does. - Quote off-chain, then pass that figure. That is what makes a fee change between your quote and your transaction cause a refusal instead of being absorbed silently, which is the entire purpose of the floor.
If you widen the floor so that small movements do not cause a refusal, whatever you subtract is the amount of fee movement you are choosing to accept without being notified.
Step 4: Trim the amount when swapping out of iBENJI
Going from iBENJI (18 decimals) to a 6-decimal stablecoin, amount must be a whole multiple of
10^12. Otherwise the swap reverts with
refused.
uint256 step = 10 ** 12;
uint256 trimmed = amount - (amount % step);
Trim before quoting, so the quote matches what you execute. The reverse direction is unaffected.
Putting it together
function doSwap(uint256 amount) external onlyOwner {
// Trim first — see above. Skipping this gets the swap refused.
// on the iBENJI → stablecoin direction, and that error masks any other
// problem, so it must be done before quoting.
uint256 trimmed = amount - (amount % (10 ** 12));
require(trimmed > 0, "amount too small");
(uint256 net, ) = ISwapPool(POOL).quoteNet(FROM, TO, trimmed, address(this));
IERC20(FROM).approve(POOL, trimmed);
ISwapPool(POOL).swap(FROM, TO, trimmed, address(this), net);
}
Quote and execute use the same trimmed figure, and net is passed straight back as
minAmountOut. The comparison is inclusive: a delivery equal to your floor is accepted, and only
a delivery below it is refused.
A floor quoted in the same transaction gives you no protection. Because both calls sit in one transaction, the quote is computed from the same state the swap settles against, so the floor always equals what is delivered and can never bind. The example above is the shape of a swap, not a slippage guard.
To actually bind the floor, quote it before the transaction — off-chain, or in an earlier block — and pass that figure in. Then a rate that moves against you between quoting and settling refuses the swap instead of settling it quietly.
The trimmed > 0 guard matters: trimming an amount below 10 ^ 12 leaves zero, which reverts as
refused for a zero amount instead — see When a swap is refused.
address(this) as the destination satisfies the self-delivery rule on either direction, so it is the
safe default.
Triggering it from outside
A contract cannot start a transaction on its own, so something has to call the entry point above. That is an ordinary transaction sent by an externally owned account to your contract — not to the pool.
Two consequences worth being precise about, because they are the ones that get mixed up:
- The signing account needs native currency for gas, and nothing else. It does not need to hold the input token, it does not need to be authorized on the pool, and it is not the address any pool check looks at. See Your contract is the trader.
- The signer triggers; the contract performs. Whatever you send from — a script, a backend service, a multisig — changes nothing about how the pool evaluates the swap.
A caller whose owner is a multisig or a timelock cannot be driven by a simple script, since the entry points are owner-gated and a multisig executes through its own transaction path. If that is your production shape, it is worth planning for before you deploy.
Key management is outside the scope of these pages — please follow your own organization's practice.
What a failure costs you
Nothing but gas. A swap either completes in full or reverts, and a revert unwinds the entire transaction — including the input transfer, which happens inside it. Your tokens are never taken and left un-swapped, and no partial fill is possible.
Only the first failed condition is reported, so fixing one can reveal another that was there all along. Work through the free reads on Check eligibility rather than one failed transaction at a time — they catch most conditions before you spend anything. See When a swap is refused for what each one means.
Once the transaction succeeds, see After the swap for the events to reconcile against.