Skip to content

Repository files navigation

Bulwark

A liquidation-defense keeper that executes onchain through KeeperHub, and declines to act when the gas is not worth it.

Built for the KeeperHub Agents Onchain hackathon.

Live snapshot → the position, the decision trail on mainnet, and the KeeperHub execution history, every row linking to a block explorer. It is a snapshot rather than a live page, and says so: the real dashboard holds an API key and so cannot be served publicly.


Reliability scorecard

Measured on Sepolia, 2026-08-02, four trials per cell. Method, caveats and raw data in chaos/RESULTS.md.

Scenario KeeperHub ethers (blind gas limit) ethers (default)
Gas underpricing (0.05 gwei vs ~0.98 market) 4/4 landed 0/4, all stuck no data
Congestion (4 concurrent from one wallet) 4/4 landed 1/4 2/4
Revert all refused all refused all refused

Underpricing is the clearest result and reproduces across runs. The baseline was pinned to a hardcoded 0.05 gwei with a hardcoded gas limit, which is what a static price becomes the moment the network moves. KeeperHub was given a thin gas budget and allowed to manage it, and landed everything; the blind baseline broadcast four transactions that never mined, confirmed from chain state and clearable only by same-nonce replacement at 5x market.

This measures managed execution against a static one-shot price. It is not a symmetric underbid, and RESULTS.md explains why an earlier version of this table claimed it was.

Congestion cost the baselines two and three transactions to -32000 "already known". KeeperHub sequenced all four.

Revert shows no difference. The public RPC refuses to relay reverting transactions, so nobody burned gas and there is nothing to separate. We report that rather than dressing it up.

The harness distinguishes three kinds of "it didn't work"

Class Meaning Effect on the rate
failed / stuck The backend under test did not deliver Counted against it
prevented Refused because the call would revert Counted, separately
excluded Our own network or RPC broke Removed from the denominator

That last row is why one cell reads "no data" instead of 0%. It also corrected a real error: an earlier run scored KeeperHub 1/3 on underpricing, which read as a reliability problem, when two of those trials were fetch failed from our own connection. With exclusions the scenario reproduces at 4/4.

Four bugs in the harness itself, including one that would have credited an RPC outage to the baseline as a safety feature, are written up in RESULTS.md.

Reproduce with npm run chaos -- --all --n 4.


What it does

People borrow against crypto collateral. If the collateral falls far enough, anyone may liquidate them: repay part of the debt, seize collateral at a discount, and the borrower eats a 5-10% penalty.

Spotting the danger is easy. Landing the rescue transaction is not: it has to execute at 3am, during a gas spike, without being front-run. Bulwark watches positions, decides whether a rescue is economically worth its gas, and executes through KeeperHub.

Its monitoring is also listed for sale on the KeeperHub marketplace at $0.01 a call, the first half of a loop where the agent's earnings pay for its own executions. The listing is live and priced; nothing has settled through it. Marketplace settlement is mainnet-only today, so closing that loop needs real USDC on Base rather than testnet funds, and this build stayed at zero capital deliberately. Treat the revenue side as built and unexercised, not as demonstrated.

The interesting part: knowing when not to act

Any agent can fire a repay when a health factor dips. Bulwark computes whether the rescue is worth the gas, and declines when it is not:

HF 1.120 → ARMED. P(liquidation) 0.7% over next 24h, loss if it happens $250.00,
expected loss $1.85. Cheapest rescue REPAY at $4.44. Ratio 0.4× vs 3× margin:
holding, gas not justified yet.

Liquidation probability uses a first-passage model, not an endpoint one: a position that dips below HF 1.0 mid-window and recovers still gets liquidated, because liquidators watch every block. By the reflection principle that roughly doubles the naive estimate.

Health factor 1.10 comes out at a ~2.3% chance of touching liquidation within a day, which lines up with how often collateral actually drops 9% in 24 hours.

Architecture

Two independent defenders watch one position. Either can act alone.

flowchart LR
    POS[("Aave V3 position<br/>Sepolia")]

    subgraph KH["Inside KeeperHub: owns CRITICAL, needs no local process"]
        direction TB
        TRIG["Block trigger<br/>every 50 blocks"] --> READ["Read<br/>health factor"] --> COND{"HF below<br/>1.05?"}
        COND -- "no" --> STOP["Stop<br/>zero transactions"]
        COND -- "yes" --> FIX["Approve LINK,<br/>supply 7 LINK<br/>onBehalfOf position"] --> VERIFY["Re-read HF<br/>to confirm recovery"]
    end

    subgraph AGENT["Local agent loop: owns ARMED, where declining is the point"]
        direction TB
        OBS["Observe over<br/>plain RPC"] --> MODEL["P(liquidation) by first passage,<br/>expected loss vs cheapest rescue"] --> DEC{"expected loss<br/>above 3x<br/>rescue cost?"}
        DEC -- "no" --> HOLD["HOLD<br/>gas not justified yet"]
        DEC -- "yes" --> RESCUE["RESCUE via KeeperHub<br/>approve, then repay or supply"]
    end

    LOG[["GuardianLog<br/>Ethereum mainnet"]]
    LEDGER[/"attestations.jsonl"/]
    DASH["Dashboard"]

    POS --> TRIG
    POS --> OBS
    FIX -. "tops up collateral" .-> POS
    RESCUE -. "repays or tops up" .-> POS

    VERIFY --> LOG
    HOLD --> LOG
    RESCUE --> LOG
    LOG --> LEDGER --> DASH
Loading

The split is deliberate. CRITICAL lives server-side because a keeper that stops defending when your laptop sleeps is not a keeper. ARMED lives in the agent because the first-passage model and the cost/benefit comparison do not fit in a condition node.

Both rescues and holds are attested, since the declined rescues are the interesting judgment calls. Attestation fires on a rescue or a tier change rather than every tick: a mainnet transaction every few minutes to report that nothing moved is noise, and it burns the execution quota.

The ledger exists because free RPC tiers stop serving eth_getLogs past a few hundred blocks, so an attestation not written down as it lands becomes unreadable within about half an hour. The chain stays the authority; the ledger is just the index.

Status

Autonomous liquidation defense is live, and every decision change is attested to Ethereum mainnet.

A KeeperHub workflow watches the position every 50 Sepolia blocks and rescues it with no local process running. Verified both branches on 2026-08-02:

Health factor Condition Outcome
1.3004 healthy false stopped after 3 nodes, zero transactions
1.0400 critical true rescued to 1.3192 in 26.5s

Onchain, all gas sponsored, all zero capital

What Chain Link
GuardianLog deployment mainnet 0x62938be3...
GuardianLog contract mainnet 0x06D8C09B...
Decision attestation mainnet 0x2d60efde...
Rescue: approve Sepolia 0xfc3555ad...
Rescue: supply Sepolia 0xc89edff9...

The mainnet attestation carries the Sepolia rescue's hash in remediationTxHash, so the public record and the economic action are linked. Decoded, it reads: HF 1.0400, action AddCollateral, expected loss $4.74, rescue cost $1.18.

KeeperHub has no deploy action, so GuardianLog went up through a CREATE3 factory, whose deploy(bytes32,bytes) is an ordinary call and therefore reachable. The address was predicted before deploying and the on-chain runtime code verified byte-identical to the local compile.

Component State
Risk tiers, probability model, cost/benefit done, 56 tests passing
Remediation selection and amount math done, verified against the live position
Position tooling (pos:* scripts) done: open, fund, danger, status, evaluate
hf-watch-critical workflow done, enabled, both branches verified
GuardianLog on mainnet deployed, attestations landing
Agent loop (ARMED tier) done: observes, decides, executes, attests, unattended
Marketplace listing done, gasguard-aave-hf-check at $0.01/call (slug predates the rename and is fixed once listed)
Chaos harness runnable end to end, 2 of 3 scenarios differentiate (RESULTS)
KeeperHub REST paths verified by probing (agent/executor.ts)
Marketplace revenue loop built, never exercised: settlement is mainnet-only and this build stayed at zero capital
Dashboard done, serves both audit trails (npm run dashboard)
Public snapshot page done, major101x.github.io/bulwark (npm run site:build)
Starter template done, first transaction needs only an API key (README)
Demo video scripted, recording pending

The open questions this depended on (Aave V3 on Sepolia, mainnet gas sponsorship for arbitrary calls, testnet marketplace settlement, free-tier quota) were answered by the KeeperHub team on Discord and are reflected throughout.

How the work is split

The CRITICAL tier runs inside KeeperHub, server-side, with a fixed-size collateral top-up. It needs no local process, so the position stays defended even when our agent is down. Economics are deliberately bypassed there: at HF 1.05 a passing gas spike is not a reason to let a position liquidate.

The agent owns the ARMED tier, where the interesting behaviour is declining to act. That reasoning is too involved for condition nodes and it is where the cost/benefit model earns its place.

Running what exists

npm install
npm test          # 56 tests, no network required
npm run typecheck

The decision logic is pure and fully testable offline: no RPC, no account, no keys.

To run the agent, copy .env.example to .env. It starts with DRY_RUN=1, which computes and logs decisions without executing anything.

npm run agent

Security

The agent never handles a private key. KeeperHub keeps them in Turnkey enclaves, and the only key in this repo's config is CHAOS_BASELINE_PRIVATE_KEY, a throwaway Sepolia key used solely by the naive baseline in the chaos harness, holding worthless testnet funds.

Layout

agent/        decision logic (risk, remediation) + execution + attestation
chaos/        failure injectors and the reliability harness
contracts/    GuardianLog.sol, the onchain audit trail
workflows/    exported KeeperHub workflow JSON
docs/         friction log (bounty input), architecture notes
site/          generator for the public snapshot page
starter-template/  zero-to-first-transaction, for the onboarding bounty

starter-template/ is standalone: copy the folder out, npm install, and npm run first-tx lands a real transaction with nothing but an API key. No testnet funds, no wallet extension, no private key.

License

MIT

About

A liquidation-defense keeper that executes onchain through KeeperHub. Decides whether a rescue is worth its gas, and attests every decision, including the holds, to Ethereum mainnet.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages