Skip to content

Repository files navigation

BFB — Panic Exit

One signature unwinds an entire DeFi position into USDC.

🏆 LI.FI Best User Experience — ETHGlobal 2026 NYC

Live: backstop-next.vercel.app


The problem

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.

What it does

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.


The hard part: order

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.


Finding the position

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:

  1. Scan labels — use loan / collateral rows if the indexer tagged them
  2. Receipt-token classification — for each dust/staked/wallet row, read UNDERLYING_ASSET_ADDRESS() off the token, resolve that reserve, and check whether the token is the reserve's aToken or variableDebtToken
  3. Full on-chain sweepgetReservesList() → multicall getReserveTokensAddresses across every Aave reserve → multicall balanceOf on 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 (balanceOftokenOfOwnerByIndexpositions), and each position is valued by simulating decreaseLiquidity to see the exact token amounts that would come out.


What can't be sold

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.


Architecture

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.

Three flow shapes

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.


Layout

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

Setup

npm install
npm run dev          # → http://localhost:3000

Backend modules run standalone:

node backend/lib/scan/run-scan.js      0xADDRESS
node backend/lib/dryrun/run-dryrun.js  0xADDRESS

Environment

Copy .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

Testing

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.


Built with

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

About

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages