One signature unwinds an entire DeFi position into USDC.
🏆 LI.FI Best User Experience — ETHGlobal 2026 NYC
Live: backstop-next.vercel.app
Capital ends up scattered — collateral in Aave, liquidity in a Uniswap V3 position, rewards unclaimed, tokens dusted across a dozen contracts. Some of it is borrowed against. Exiting means a dozen transactions in exactly the right order, and getting the order wrong reverts everything.
People don't get liquidated because they're insolvent. They get liquidated because they're slow.
SCAN → DRY-RUN → BUNDLE → SIGN & SEND
| Stage | What happens | Cost |
|---|---|---|
| SCAN | Discover every position — debt, collateral, LP, rewards, dust | free (reads) |
| DRY-RUN | Price the whole exit → "you'll receive ≥ $X USDC" | free (reads) |
| BUNDLE | Compile the ordered unwind into one Composer flow | free (no broadcast) |
| SIGN & SEND | One signature. One transaction. Everything settles in USDC. | gas |
Nothing touches the chain until the final signature.
You can't withdraw collateral while a loan sits against it. The entire unwind is
compiled as a single atomic flow, flash-loan wrapped, in this order
(lib/exit/flows/exit/flow.ts):
1. flashloan the debt asset (Aave v3, via Composer)
2. aave.repay → frees the collateral
3. pull aTokens from the EOA (only if position is on the EOA)
4. lifi.zap aToken → underlying → the Aave withdraw
5. swap underlying → USDC
6. LP leg: decreaseLiquidity → collect → swap both tokens
7. claim rewards → swap
8. sweep remaining tokens → swap
9. flashloanRepay (principal + Aave premium)
Every step is bound to the output of the step before it, so amounts are resolved at execution time rather than guessed. If any leg fails, the whole transaction reverts — you are never left half-unwound.
Debt is padded before the flashloan (padForAaveDebtRounding, plus a 5bps
accrual buffer for EOA positions) so interest accruing between simulation and
execution can't leave a dust debt behind.
The scan layer can't be trusted to label an Aave position correctly — freshly
supplied positions often surface as plain wallet tokens with no price. So
lib/exit/integration/resolve.ts resolves it in three tiers, falling through
until something works:
- Scan labels — use
loan/collateralrows if the indexer tagged them - Receipt-token classification — for each
dust/staked/walletrow, readUNDERLYING_ASSET_ADDRESS()off the token, resolve that reserve, and check whether the token is the reserve's aToken or variableDebtToken - Full on-chain sweep —
getReservesList()→ multicallgetReserveTokensAddressesacross every Aave reserve → multicallbalanceOfon every aToken and debt token → take the largest of each
Tier 3 finds an Aave position with zero dependence on any indexer. Debt is always
read as variableDebtToken.balanceOf(signer) — never a cached USD figure.
Uniswap V3 LPs are enumerated straight from the NonfungiblePositionManager
(balanceOf → tokenOfOwnerByIndex → positions), and each position is valued
by simulating decreaseLiquidity to see the exact token amounts that would
come out.
Some positions cannot be exited atomically — locked veCRV, vlCVX, unstETH
withdrawal NFTs, Frax redemption queue entries. These are matched against a known
set (flows/exit/lst.ts) and routed to nonAtomic rather than silently valued
as if they were liquid. The payout figure never counts an asset the flow can't
actually sell.
Alchemy RPC → balances, token metadata, USD prices, all on-chain reads
Zerion API → alternative position discovery path
Uniswap V3 → LP enumeration + decreaseLiquidity / collect
LI.FI Composer→ compiles and executes the ordered unwind as one transaction
Aave v3 → flashloan provider, repay, withdraw, reward claims
Swaps inside the flow execute through lifi.swap, routed across an allow-listed
set of aggregators (flows/exit/constants.ts). Uniswap's role in the flow itself
is the V3 position manager — the LP leg — not the swap venue.
| Builder | When | Shape |
|---|---|---|
buildExitFlow |
position has debt | flashloan → repay → withdraw → sell → LP → claims → sweeps → repay flashloan |
buildSweepOnlyFlow |
no debt | claims + sweeps → USDC, directDeposit materialisers |
buildLpExitFlow |
LP only | pull the NFT into the proxy → decrease → collect → sell |
Simulation policy is strict where amounts are knowable ahead of time, and
allow-revert with explicit assumptions where they aren't (EOA collateral
pulls, LP exits) — because the real amounts only exist after simulation.
app/ Next.js routes
components/BackstopWireframe.jsx the client flow — all screens
hooks/ wallet + data hooks
backend/lib/
scan/
onchainScan.js Alchemy balances + prices + LP resolution
scan.js Zerion-based scan (paginated, 6-category normaliser)
aave.js aToken / variableDebtToken lookup
dryrun/
dryrunOnchain.js nets assets vs on-chain debt → guaranteed minimum
dryrun.js Zerion-path dry-run
uniswap-quote.js Uniswap Trading API quotes
lib/exit/
config.ts RPC + contract addresses
onchain/abi.ts Aave, ERC20, Uniswap NPM ABIs
onchain/aaveRounding debt padding + flashloan premium math
integration/resolve.ts three-tier position resolution
flows/exit/
flow.ts the ordered unwind (appendExitOps)
index.ts the three flow builders
lpLeg.ts decreaseLiquidity + collect + sell
sweep.ts token sweeps
claim.ts Aave / Curve / Convex / Comet reward claims
lst.ts LST + locked-position registries
constants.ts slippage, allowed exchanges
npm install
npm run dev # → http://localhost:3000Backend modules run standalone:
node backend/lib/scan/run-scan.js 0xADDRESS
node backend/lib/dryrun/run-dryrun.js 0xADDRESSCopy .env.example to .env and supply your own keys. Never commit .env.
ALCHEMY_RPC_URL= # all on-chain reads + prices alchemy.com
LIFI_API_KEY= # Composer li.fi
LIFI_COMPOSER_BASE_URL= # Composer endpoint
ZERION_KEY= # optional, Zerion scan path zerion.io
UNISWAP_API_KEY= # optional, quote path developers.uniswap.org
FORK_RPC_URL= # optional, defaults to http://127.0.0.1:8545
LI.FI has no testnet support — the bridges and aggregators it routes through
have no testnet liquidity. The exit flow is therefore tested against a mainnet
fork, where every contract is the real deployment and state matches production,
but nothing costs real funds. Point FORK_RPC_URL at your fork.
LI.FI Composer — flow compilation and execution · Aave v3 — flashloan, repay, withdraw, rewards · Uniswap V3 — LP position manager · Alchemy — RPC, token metadata, prices · Zerion — position discovery · Next.js · viem · TypeScript