Destination Override Approval
Sending swap output to an address other than your own contract needs more than that address being authorized on the pair. Your contract must also grant itself a per-destination approval on the pool, and that approval carries a budget and a deadline.
This applies only when you name a third-party destination. Swapping to your own contract needs none of it.
Why your contract has to grant it, not you
The pool keys the approval on msg.sender. In a contract-to-contract swap your contract is
msg.sender, so the approval has to be granted by the contract itself. Calling the pool from your
owner wallet writes a record the swap will never read.
The approval is keyed on the exact four-part route:
(trader, fromToken, toToken, destination)
Change any one of the four and it is a different record.
The errors
| Error | Meaning |
|---|---|
| never granted | no approval record has ever existed for this exact route |
| expired | a record exists but its deadline has passed |
| budget exhausted | a record exists and is in date, but the remaining budget is smaller than the amount you asked for |
These three are mutually exclusive and they tell you different things — read them as a state, not as one failure.
Reading the record before you swap
getDestinationOverrideApproval(trader, fromToken, toToken, destination)
→ (deadline, remainingAmount)
Two details are worth getting right, because either one returns (0, 0) — the same result as an
approval that was never granted:
traderis the first argument, not the third.- The return is
(deadline, remainingAmount)— deadline first.
Checking the result against a value you can predict is a quick way to confirm you have both right.
deadline | remainingAmount | State |
|---|---|---|
0 | 0 | never granted |
| in the past | anything | expired |
| in the future | 0 | drained — the budget is spent |
| in the future | > 0 | usable, up to remainingAmount |
Granting an approval
Two errors come from the grant itself rather than the swap:
| Error | Meaning |
|---|---|
a budget of 0 | not accepted — grant a real amount |
| a deadline earlier than the current block | not accepted — the deadline may be the current block or later |
The current block itself is accepted: an approval granted that way is usable in that block and expired from the next. So you cannot create an approval that was already expired when granted — to see the expiry behavior you have to grant a short deadline and let it pass.
A block number you read off-chain is already stale by the time your transaction mines. Add a margin rather than passing the value you read unchanged, or the grant can land expired — or be rejected outright if the chain has moved past it.
Spending is one-way
Consuming an approval reduces remainingAmount but never clears deadline. So once a route has
been granted even once, it can never again be reported as never granted — only as expired or
exhausted.
Re-granting resets both fields, so an exhausted route is reusable. Partial spends work as you would expect: a 1.0 budget spent 0.4 leaves 0.6, and a subsequent request for 0.8 is rejected while 0.6 succeeds.
Grant and swap together, or separately
Granting and swapping in one transaction is the safer pattern, and the one to default to. The approval cannot be consumed by anything else in between, and it is left drained and past its deadline as soon as the block closes. The caller contract template does exactly this, and offers no way to do otherwise.
Splitting them is legitimate — you might want to grant a budget once and spend it across several swaps. Two things to know before you design for it. Your contract needs a function that grants without swapping, and a swap path that does not re-grant, otherwise the next swap simply overwrites the budget you set. And the deadline is then real elapsed blocks, so an approval you granted earlier may have expired before you use it.
What is checked before any of this
Two things are settled before your approval is even looked at: whether the output token may go to a third party at all, and whether the recipient is authorized on the pair. If the output is a stablecoin, no approval of any kind will help — stablecoin output can only go to the account that did the swap. Deliver to your own contract and transfer onward. See When a swap is refused.
Your own contract may also apply its own destination checks before it ever calls the pool. Those are yours, not the pool's, and the pool's errors say nothing about them.