Intents & Batching
Paragon Flow upgrades swapping from “send a transaction and hope” to sign an intent, batch with others, settle with best execution, and share the surplus. It’s the engine behind Paragon Shield’s.
TL;DR
You sign an EIP-712 Intent (not a transaction).
A Batcher groups intents for a short window and asks Solvers to propose a settlement.
The best proposal is executed on-chain through FlowSettlement, which:
enforces your bounds (
minOut
, deadline, receiver),computes surplus = amountOut − minOut,
splits surplus to Trader / LPs used / stXPGN lockers,
emits auditable events (Proof-of-Best-Execution).
If no solver can beat your bounds, the batch reverts for you (or, if you opted in, falls back to a normal router swap).
Why intents?
MEV resistance: batched clearing + private relay kills most sandwich vectors.
Better pricing: solvers can route multi-hop across multiple venues and cross-match flows.
Programmable refunds: surplus is shared to the right parties automatically.
Key actors
Trader: signs an Intent.
Batcher/Relay: groups intents, broadcasts to whitelisted solvers.
Solver(s): search for best settlement across Paragon pools (+ external DEXes later).
FlowSettlement: on-chain verifier/executor; distributes surplus to vaults.
The Intent (EIP-712)
// EIP-712 domain
const domain = {
name: "ParagonFlow",
version: "1",
chainId, // BNB Testnet during beta
verifyingContract: FLOW_SETTLEMENT_ADDRESS,
} as const;
// EIP-712 types
const types = {
Intent: [
{ name: "sellToken", type: "address" },
{ name: "buyToken", type: "address" },
{ name: "amountIn", type: "uint256" },
{ name: "minOut", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "receiver", type: "address" },
{ name: "prefs", type: "uint256" }, // bitfield (privacy, fallback, slippage mode)
{ name: "nonce", type: "uint256" },
],
} as const;
// Example value
const value = {
sellToken, buyToken,
amountIn, // exact-in
minOut, // bound after fees
deadline, // block timestamp bound
receiver: user,
prefs, // see "Preferences bitfield" below
nonce, // wallet-defined
};
Preferences bitfield (prefs
)
prefs
)0..1
privacy: 0=Lite, 1=Private, 2=Stealth2
fallback: 0=No fallback (revert), 1=Fallback to router if solver fails3
cashback mode: 0=claim to wallet, 1=auto-stake to stXPGN4..7
reserved
You can cancel any outstanding intent by incrementing your nonce via
FlowSettlement.cancel(nonce)
or signing a replacement with a higher nonce.
Batching windows
Window: ~1 block on BNB (testnet setting), adaptive later.
Priority: FIFO within the same gas price band.
Max batch size: capped to keep gas predictable (configurable).
Liveness: if no valid solver submission before window end → optional fallback or revert.
Auction & scoring
Solvers submit a Settlement:
{ routeData, expectedOut, gasEstimate, lpTouchSet, proof }
Score = (expectedOut − minOut) − gasCostEquivalent − penalties
expectedOut − minOut → higher is better (more surplus).
gasCostEquivalent → convert gas to output token via oracle.
penalties → for unhealthy routes (too many hops, stale pools, unsafe external venues).
Tie-breakers: fewer hops → earlier timestamp → lower variance in sim.
Batcher picks the top-score, builds a bundle, and sends it to FlowSettlement.execute
.
On-chain settlement (Solidity surface)
interface IFlowSettlement {
struct Intent {
address sellToken;
address buyToken;
uint256 amountIn;
uint256 minOut;
uint256 deadline;
address receiver;
uint256 prefs;
uint256 nonce;
}
function execute(
Intent calldata intent,
bytes calldata signature,
bytes calldata routeData // ABI-encoded hops & venues
) external returns (uint256 amountOut);
function cancel(uint256 newNonce) external;
event ShieldSubmitted(address indexed user, bytes32 intentHash);
event ShieldSettled(
address indexed user,
bytes32 indexed intentHash,
uint256 amountOut,
uint256 surplus, // amountOut - minOut (>=0)
uint256 traderRebate, // default 60%
uint256 lpRebate, // default 30%
uint256 lockerCut // default 10% -> stXPGN
);
}
Settlement checks
signature & domain match
block.timestamp ≤ deadline
user allowance/transferFrom succeeds for
amountIn
Router performs the
routeData
swaps atomicallyamountOut ≥ minOut
else revert (or fallback if allowed)
Surplus & payouts
Surplus:
surplus = amountOut − minOut
(0 if negative).Splits (default):
60% → Trader (instant claim or auto-stake to stXPGN)
30% → LPRebateVault (credited to pairs actually touched by the route)
10% → LockerVault (stXPGN / veFlow lockers)
Protocol can take a small cut (0–5%) of surplus before splits (DAO-governed).
LPRebateVault accounting
We snapshot pair, blockNumber, usedLiquidity for the settlement.
LPs claim proportionally to the liquidity they had in that block.
External venues (phased)
Phase 0 (testnet): Paragon pools only.
Phase 1: add whitelisted Pancake/Thena routes with per-venue caps and slashing bonds for executors.
Phase 2: open allow-list, higher caps, cross-DEX batch netting.
Fallbacks & failures
Revert (default) if no solver produces
amountOut ≥ minOut
.Fallback to Router if the
prefs
bit is set. UI shows: “Shield unavailable, executed via Router.”Partial batch failure: single intent revert doesn’t affect others; we isolate by executing intents independently (no shared state assumptions).
Security model
Permissions: FlowSettlement is upgrade-gated by timelock + multisig.
Solver bonds: executors post bonds and can be slashed (mis-pricing, invalid routing, griefing).
Replay protection: per-user nonces + domain separation.
Oracle usage: only for gas costing and sanity checks; user bound (
minOut
) is the real guard.Emergency: Shield can be paused without affecting classic swaps.
Relay API (testnet draft)
Submit intent
POST {RELAY_URL}/v1/intent
{
"intent": { ...EIP712 value... },
"signature": "0x...",
"hint": { "privacy": "lite|private|stealth", "fallback": true|false }
}
→ { "intentHash": "0x...", "batchEta": 1730000000 }
Status
GET {RELAY_URL}/v1/intent/{intentHash}
→ { "state": "pending|settled|failed|reverted", "txHash": "0x...", "savingsPct": "0.42" }
WebSocket feed
wss://.../v1/stream
→ ShieldSubmitted
/ ShieldSettled
notifications.
Frontend integration (quick guide)
Prepare intent & sign
import { signTypedData } from "viem/actions";
const signature = await signTypedData(wallet, { domain, types, primaryType: "Intent", message: value });
POST to relay (or call
execute
directly if you run a solver).Show live status (pending → settled).
If settled, extract surplus + rebate amounts from event or status and surface “You saved X%”.
Claims
Trader: call
TraderRebateVault.claim(user)
if not auto-staked.LPs: show accrued Flow earnings on the pool page (per-block snapshots).
Testnet parameters (current)
Chain: BNB Testnet
Batch window: ~1 block (adaptive 1–2 blocks if congestion)
Min trade size: none (UI may nudge > $5)
Max hops: 4 (Paragon pools only)
Surplus split: 60/30/10 (TBD via DAO for mainnet)
Contracts:
FlowSettlement
:FLOW_SETTLEMENT_ADDRESS
TraderRebateVault
:TRADER_REBATE_VAULT
LPRebateVault
:LP_REBATE_VAULT
LockerVault
:LOCKER_VAULT
Developer FAQs
Q: Can I cancel/replace an intent?
Yes. Call cancel(newNonce)
or sign a newer intent with a higher nonce
.
Q: Does batching increase gas? Settlement for a batch is similar to one swap with comparable hops. Slightly higher overhead is typically offset by better price (surplus).
Q: How are LP rebates mapped to pairs?
Solvers must provide lpTouchSet
per hop; settlement verifies via router events and credits the vault.
Q: Can I force a public (non-Shield) swap?
Yes—use classic Router or set prefs.fallback=true
and toggle Shield off in the UI.
Roadmap hooks
Limit/RFQ intents
Time-weighted auctions for large orders
Cross-DEX netting (match internal flows to reduce market impact)
veFlow voting to steer surplus weights and utilization emissions
Last updated