Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Liquidation keeper

Template: liquidation-keeper Β· category: keeper Β· risk: money_moving

A read trigger polls healthFactor(borrower) on a lending pool; when it crosses below the threshold the keeper re-reads the position and fires a simulated, gas-capped liquidate(address). Runnable sibling: examples/liquidation-keeper.

When to use it

  • run a liquidation bot against Aave-style pools with healthFactor-shaped views
  • any "poll a view function, send when a threshold crosses" keeper (adapt the function and condition)

Generate it

rflow new --template liquidation-keeper
# or into an existing project (needs a signer; adds the `keeper` relayer):
rflow add workflow liquidation-keeper

What the generated YAML does

  1. Trigger — polls healthFactor(borrower) every poll_every (default 15s) and fires on the false→true crossing of output < wei(min_health, 18) (mode: threshold is edge-triggered — a position sitting underwater does not re-fire every poll).
  2. health β€” a fresh read right before the money step; a position repaid between the poll and now makes the run succeed quietly.
  3. liquidate β€” gated by if: on the fresh read, simulated as the keeper (a healthy position reverts in simulation and nothing is broadcast), assert_sim checks sim.ok and a sim.gas_used cap, and the gas policy caps price and limit. The simulation is also the healed-position guard at broadcast time β€” a position repaid after the health read reverts in eth_call and dead-letters instead of broadcasting.

Inputs

keytypedefault
project_namestringliquidation-keeper
network / chain_id / rpc_env / rpc_urlnetwork / chain_id / env_var / stringethereum / 1 / ETH_RPC / a public RPC
confirmationsint3
pool_addressaddresszero placeholder β€” replace it
borroweraddresszero placeholder β€” replace it
poll_everyduration15s
min_health / health_decimalstoken_amount / int1 / 18
gas_max_pricestring50 gwei
sim_gas_capint300000

Required env vars

DATABASE_URL, the RPC env var, RAW_DANGEROUS_MNEMONIC. rflow new fills .env with a freshly generated DEV-ONLY mnemonic β€” swap in a production signer before real funds ride on this config.

Safety defaults (all generated)

  • edge-triggered poll (mode: threshold) β€” one run per crossing
  • fresh read + if: gate before the send
  • pre-flight simulation + assert_sim: ["${{ sim.ok }}", "${{ sim.gas_used < 300000 }}"] β€” this is the healed-position guard: liquidating a healthy position reverts in simulation instead of broadcasting (a recheck would only re-test the journaled health read the if: gate already passed, so the template ships none)
  • gas cap: limit_from_simulation, multiplier: 1.3, max_price
  • concurrency.on_conflict: skip β€” one liquidation in flight at a time
  • wait_for: confirmed, on_failure: dead_letter

Run it locally

docker compose up -d           # postgres on :5448
rflow validate                 # should be green out of the box
rflow test liquidation-keeper --fixture fixtures/underwater-read.json  # dry-run rehearsal
rflow start                    # go live

The generated fixtures/underwater-read.json is a 0.9 health factor β€” rflow test dry-runs the whole pipeline (including the simulated send) without broadcasting.

Production checklist

  1. replace the pool_address and borrower placeholders
  2. replace the raw dev mnemonic with a production signer
  3. fund the keeper relayer with gas; verify the liquidation bonus covers your gas_max_price worst case
  4. tighten poll_every to your latency budget (every poll is an RPC call)
  5. add a notification channel on the dead-letter path so failed liquidations page someone
  6. cap the blast radius of a bad deploy or oracle glitch with a durable budget on the liquidate step (e.g. a max number of liquidations / total gas per hour), and add an approval: gate if a human should sign off before the first live liquidation

Common modifications

  • Many borrowers: move borrower into a list: seeded by an event workflow, and foreach over it.
  • Profit guard: read the collateral price first and add an if: comparing expected bonus to sim.gas_used * gas price.
  • Partial liquidations: swap liquidate(address) for your pool's liquidationCall(...) signature with an amount argument.
  • Faster sends: set the relayer speed: FAST (see examples/liquidation-keeper).