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-keeperWhat the generated YAML does
- Trigger β polls
healthFactor(borrower)everypoll_every(default 15s) and fires on the falseβtrue crossing ofoutput < wei(min_health, 18)(mode: thresholdis edge-triggered β a position sitting underwater does not re-fire every poll). healthβ a fresh read right before the money step; a position repaid between the poll and now makes the run succeed quietly.liquidateβ gated byif:on the fresh read, simulated as the keeper (a healthy position reverts in simulation and nothing is broadcast),assert_simcheckssim.okand asim.gas_usedcap, and the gas policy caps price and limit. The simulation is also the healed-position guard at broadcast time β a position repaid after thehealthread reverts ineth_calland dead-letters instead of broadcasting.
Inputs
| key | type | default |
|---|---|---|
project_name | string | liquidation-keeper |
network / chain_id / rpc_env / rpc_url | network / chain_id / env_var / string | ethereum / 1 / ETH_RPC / a public RPC |
confirmations | int | 3 |
pool_address | address | zero placeholder β replace it |
borrower | address | zero placeholder β replace it |
poll_every | duration | 15s |
min_health / health_decimals | token_amount / int | 1 / 18 |
gas_max_price | string | 50 gwei |
sim_gas_cap | int | 300000 |
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 (arecheckwould only re-test the journaled health read theif: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 timewait_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 liveThe 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
- replace the
pool_addressandborrowerplaceholders - replace the raw dev mnemonic with a production signer
- fund the
keeperrelayer with gas; verify the liquidation bonus covers yourgas_max_priceworst case - tighten
poll_everyto your latency budget (every poll is an RPC call) - add a notification channel on the dead-letter path so failed liquidations page someone
- cap the blast radius of a bad deploy or oracle glitch with a durable
budget on the
liquidatestep (e.g. a max number of liquidations / total gas per hour), and add anapproval:gate if a human should sign off before the first live liquidation
Common modifications
- Many borrowers: move
borrowerinto alist:seeded by an event workflow, andforeachover it. - Profit guard: read the collateral price first and add an
if:comparing expected bonus tosim.gas_used * gas price. - Partial liquidations: swap
liquidate(address)for your pool'sliquidationCall(...)signature with an amount argument. - Faster sends: set the relayer
speed: FAST(seeexamples/liquidation-keeper).