Skip to content

feat: Liquity V1 as the eUSD CDP venue (#39) - #60

Merged
adachi-440 merged 8 commits into
mainfrom
feat/liquity-cdp-venue
Aug 12, 2026
Merged

feat: Liquity V1 as the eUSD CDP venue (#39)#60
adachi-440 merged 8 commits into
mainfrom
feat/liquity-cdp-venue

Conversation

@adachi-440

Copy link
Copy Markdown
Contributor

Closes #39. Closes #37.

The leverage surface was Aave alone — pool-based borrowing with a per-position health factor. This
adds a CDP stablecoin: Liquity V1, forked unmodified, issuing eUSD. Phase 1 (the deployer) was
already on this branch; this is everything that makes it a venue an agent can trade.

What is here

adapter sdk/src/protocols/liquity.tsreadState / observe / buildTxs / valueUsdc / valueAtBlock, and eight actions
observation own ICR, system TCR, Recovery Mode, position in the redemption queue, Stability Pool deposit and ETH gain, both fee curves, the eUSD market price against par, native-ETH gas headroom
scoring the historical read path (#41's staged reads), with eUSD marked at what the pool would pay
agents redemption-arb (the venue's α), trove-manager (the borrower), sp-underwriter (the Stability Pool)
regimes liquity (redemption arb) and liquity-crash (borrower / underwriter)
environment the oracle repoint, per-block telemetry, liquidation ground truth from the venue's own logs, and the eusdDepeg stress event

eUSD is never assumed to be worth a dollar

It is deliberately absent from the token registry. Registering it as a stable would have the
scorer's spot sweep price it at par — and a CDP stablecoin trading at 0.98 marked at 1.00 hands
every holder value they cannot realize, which is the phantom-value failure the no-arb guard exists
to catch. Every mark comes from the pool: the headline value at the two-sided probe mid, the
realizable one at what the agent's own size would fetch, with the debt leg quoted through get_dx
because a liability is sized on the output side. The 200 eUSD of gas compensation is excluded from
the borrower's liability, and a Trove under 100% is clamped at zero — the owner can walk away from
it, which is a real property of a CDP rather than an accounting convenience.

Four things had to be measured, and three of them changed the design

The pool could not depeg. At A=2000, selling half the eUSD side of a 100k/100k pool moves the
price 4.4bps, while redemption costs a 50bps floor fee — no plausible flow could ever open the
venue's own arbitrage. At A=100 the same pool moves 22bps on a 10k sale and 114bps on 40k, which
keeps the stableswap's peg-then-cliff shape at a scale agents holding 25k of capital can reach.

Redemption reverted every single time. Liquity checks a partial redemption against a hint
derived from the price the transaction itself fetches, and this environment writes a new price every
block — ahead of every agent, since the oracle write bids the top of the block. Hints computed off
chain are stale by construction. LiquityRedemptionHelper computes them after fetchPrice() has
cached the price the redemption will use. It is periphery in the sense HintHelpers is; the core is
still unmodified.

The reference agent closed its own trade. Its first live run bought at a 120bps discount, its own
fill left 78bps, and 85bps was needed to redeem — so it sat on the inventory and exited through the
pool, never touching the redemption path it exists to exercise. It now sizes the buy to the headroom
above that threshold.

Liquidation ordering — the open question #39 left as "measured, not assumed". The Trove crossed
MCR at block 982; the agent, reading a one-block-old observation, saw it at 983; the liquidation
landed at 984. Two blocks, one of them the observation lag and one the mempool — the same as every
other venue here. Unlike a partial redemption, liquidate() carries no value that must still match
at execution, so a price that recovers costs the caller a reverted transaction rather than breaking
the path. No special handling needed, and the redemption helper has no counterpart on that side.

The venue is inert without something to trade

The deploy seeds the eUSD/USDC pool at par, where sitting still is correct. eusdDepeg is the stress
event that puts the peg somewhere else: the environment sells eUSD into its own market for a window
and buys it back, reconciled to a per-block target like the liquidity pull (#52), because a one-shot
gets stranded by a dropped block. Its magnitude range straddles break-even on purpose — a range
where every draw pays would measure reaction time rather than judgement.

Deviations from the issue, all deliberate

  1. Eight actions, not seven. Without liquitySwapEusd the venue's own α is unreachable except
    through hand-rolled rawTx.
  2. The fork is built, not vendored. Phase 1 sparse-clones the source at a pinned commit and
    builds it with Foundry in 6.3s, rather than committing prebuilt artifacts.
  3. One contract beyond the price feed — the redemption helper above.
  4. Pool A is 100, not the USDC/DAI pool's 2000 — see the measurement.
  5. LQTY gains are not valued. The scope line asks for "SP deposit plus pending ETH/LQTY gains";
    the issue's own design decision says LQTY gets no market and no valuation. The latter wins, and
    the observation still reports the gain. A live run confirmed it: 61.3 LQTY accrued to a depositor
    and was reported as an unaccounted holding rather than silently valued at zero.
  6. prompt.md, not both files. The issue asks the reference agent to carry agent.ts and
    prompt.md because the committed roster ran agents in prompt mode — a premise ADR 0018 removed.
    This PR renames the improvement policy back to prompt.md (ADR 0018 Amendment 1) and closes the
    name collision with a required kind: improve marker, so an old per-decision prompt is refused
    rather than reinterpreted as a policy for revising strategies.

Also here

MIT (closes #37). There was no LICENSE file, no license field in any of the five
package.json files and nothing in the README — an unlicensed public repository grants nothing,
which is the opposite of what a competition that says "copy example/agents/<id>/" needs.
THIRD-PARTY.md records the two things in the tree that keep their own terms. The premise that made
#37 block #39 does not hold: all 81 of Liquity's contract sources are SPDX-License-Identifier: MIT
and vendor/liquity-src/ is gitignored, so nothing of it is redistributed here.

What this does not do

Recovery Mode is implemented and observable but unreachable — the 300% genesis Trove dominates
the system ratio, so TCR bottoms at 2.244 against a CCR of 1.5. Every route to it multiplies system
debt, which dilutes the redemption fee curve and the Stability Pool's relative depth — both
properties measured here. Split out as #59 rather than changed as a side effect.

trove-manager and sp-underwriter shipped despite being listed as follow-on, because without a
Trove in existence six of the eight actions had never executed on chain and the ordering question
could not be answered at all.

Verification

  • 437 tests (4 skipped), typecheck, check:strategy, check:boundaries — all green
  • All eight actions have executed on chain, across the two regimes, with no reverts:
    openTrove 12 · adjustTrove 24 · closeTrove 2 · redeem 51 · provideToSP 49 ·
    withdrawFromSP 3 · liquidate 3 · swapEusd 69
  • liquity regime: the full loop — buy 1,822 eUSD at 120bps, redeem, wrap, sell — with the
    redemption fee moving 50 → 158bps behind it. redemption-arb +57.81 against a noop baseline
  • liquity-crash regime: a real liquidation (ground truth from TroveLiquidated). The defenceless
    borrower lost 20 ETH and kept ~48k USDC (-13,140, which is the price move plus fees and exactly
    what a CDP is supposed to cost); the underwriter made +4,580 on the collateral it absorbed
  • both regimes replay through npm run backtest; the state dump was regenerated with the venue

🤖 Generated with Claude Code

adachi-440 and others added 8 commits August 12, 2026 12:23
The leverage surface was Aave alone — pool-based borrowing with a per-position health
factor. A CDP stablecoin adds a class of skills nothing else reaches: redemption arb
against the riskiest Trove, Stability Pool underwriting, Recovery Mode as a system-wide
threshold that moves everyone's risk at once, and defending a position in the sorted
list.

The core is forked unmodified. Recovery Mode, redistribution, the sorted list and both
fee curves are the game the venue adds; simplifying any of them produces something else.

**Two of the issue's premises turned out not to hold, and the approach changed.**

*Licensing.* The issue treats #37 as a hard blocker because "Liquity V1 is GPL-3.0".
GPL-3.0 is the repository's LICENSE file; every one of the 81 contract sources carries
`SPDX-License-Identifier: MIT`, and those are the files this build consumes.

*Vendoring.* The issue proposes committing prebuilt artifacts to keep solc 0.6.11, old
Hardhat and Node 14–16 out of the deployer. Foundry compiles the whole system in 6.3
seconds — Liquity has no external dependencies, its SafeMath and Ownable are vendored
copies under Dependencies/ — so setup-vendors.sh sparse-clones the source at a pinned
commit and builds it in place, like gmx-src and curve-src. Nobody else's code lands in
this repository, and the artifacts cannot drift from the source that produced them.

**The oracle is ours.** Liquity's own PriceFeedTestnet has an unpermissioned `setPrice`,
which would let any agent set the price it is being liquidated against. LiquityPriceFeedAdapter
serves Liquity's IPriceFeed from the environment's PriceFeed (ADR 0006 §3) instead. It has
to exist as a level of indirection because Liquity renounces ownership after wiring — the
oracle address baked into TroveManager is permanent — while the environment deploys a
fresh PriceFeed every run. Repointing is gated on the simulation's admin key, the same
arrangement as the LST vault (issue #38). It never reverts: a revert there would freeze
borrowing, liquidation and redemption alike, so an unreadable source keeps serving the
last good price, as Liquity's own feed does when Chainlink breaks.

**The genesis Trove is ours too**, because LUSDToken has no admin mint: every eUSD in
existence has to come out of somebody's Trove. 150 ETH against 200,000 eUSD (225% ICR)
mints the supply that seeds a 100k/100k eUSD/USDC pool on the stableswap factory that
already hosts USDC/DAI, plus 50,000 eUSD of Stability Pool depth. At 225% it is also the
last Trove redemptions would reach, so agent-opened Troves are what redemption arb
actually competes over.

The 14-day BOOTSTRAP_PERIOD is cleared by warping, not by patching a constant.

Verified on a live chain, and asserted at deploy time so a state dump cannot be taken on
a venue that does not work: the stablecoin really is renamed (the rename is a `sed`, which
silently does nothing against a tree that has moved on), the system does not open in
Recovery Mode, and a redemption simulates cleanly — which is the check that matters, since
if the warp ever stops working everything else still passes while the venue loses its
reason to exist. A real 1,000 eUSD redemption during development moved the Trove's debt
201,200 → 200,200, paid out 0.33 ETH, and took baseRate 0 → 0.25%.

Deployer only. The SDK adapter, observation, scoring path and the redemption-arb agent
are the phases after this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ring, redemption arb (#39 phases 2-5)

Phase 1 stood the venue up on chain. This is the half that makes it a venue anyone can
trade: the SDK adapter and its eight actions, the observation, the historical valuation
path, and the reference agent that closes the loop the issue is named for.

**eUSD is never assumed to be worth a dollar.** It is deliberately absent from the token
registry, because registering it as a stable would have the scorer's spot sweep price it
at par — and a CDP stablecoin trading at 0.98 marked at 1.00 hands every holder value
they cannot realize, which is exactly the phantom-value failure the phantom-spread guard
exists to catch. Every mark here comes from the pool: the headline value at the two-sided
probe mid, the realizable one at what the agent's own size would fetch, with the debt leg
quoted through `get_dx` because a liability is sized on the output side. The 200 eUSD of
gas compensation is excluded from the borrower's liability, and a Trove under 100% is
clamped at zero rather than going negative — the owner can walk away from it, which is a
real property of a CDP rather than an accounting convenience.

**Three things had to be measured rather than assumed, and two of them changed the design.**

*The pool could not depeg.* At A=2000, selling half the eUSD side of a 100k/100k pool
moves the price 4.4bps. Redemption costs a 50bps floor fee, so no plausible flow could
ever open the venue's own arbitrage. At A=100 the same pool moves 22bps on a 10k sale and
114bps on 40k, which keeps the stableswap's peg-then-cliff shape at a scale agents holding
25k of capital can reach.

*Redemption reverted every single time.* Liquity checks a partial redemption against a
hint derived from the price the transaction itself fetches, and this environment writes a
new price every block — ahead of every agent, since the oracle write bids the top of the
block. Hints computed off chain are stale by construction. `LiquityRedemptionHelper`
computes them after `fetchPrice()` has cached the price the redemption will use. It is
periphery in exactly the sense HintHelpers is; the core is still unmodified.

*The reference agent closed its own trade.* Its first live run bought at a 120bps
discount, its own fill left 78bps, and 85bps was needed to redeem — so it sat on the
inventory and exited through the pool, never touching the redemption path it exists to
exercise. It now sizes the buy to the headroom above that threshold. The loop measured
end to end: buy 1,822 eUSD at 120bps, redeem, wrap, sell — and the redemption fee moves
50 -> 158bps behind it, so the first mover prices everyone who follows.

**The venue is inert without something to trade.** The deploy seeds the pool at par, where
sitting still is correct. `eusdDepeg` is the stress event that puts the peg somewhere
else: the environment sells eUSD into its own market for a window and buys it back,
reconciled to a per-block target like the liquidity pull, because a one-shot gets stranded
by a dropped block. Its range straddles break-even on purpose — a range where every draw
pays would measure reaction time rather than judgement.

Two deviations from the issue, both deliberate. It lists seven actions; there are eight,
because without `liquitySwapEusd` the venue's own alpha is unreachable except through
hand-rolled rawTx. And it requires the agent to ship `prompt.md` as well as `agent.ts` —
prompt mode was retired in ADR 0018, so the agent ships `improve.md` instead.

Verified on a live chain: redemption-arb +57.81 against a noop baseline, four transactions,
no reverts, no violations; the system debt falls where the redemption lands; and the two
scoring marks separate exactly where the position is size-sensitive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seeding every venue spends about 999,000 of the deployer account's 1,000,000 ETH, so a
second deploy onto the same chain dies wrapping WETH with `insufficient funds` — several
minutes in, and with an error that says nothing about the cause.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Amendment 1)

ADR 0018 split the name because the meaning had inverted: the retired prompt.md answered
"given this observation, what do you do", and the file that replaced it answers "when, on
what evidence, and how should the strategy change". But only one kind of LLM instruction
can sit in an agent directory now, so there is no second name for a participant to learn —
only a name that does not match what the guides, the issues and the submission format all
call it.

So the name goes back, and the collision it re-opens is closed in the format rather than
by hoping. Nineteen files of the old kind were deleted in f42fd2a; they still exist in git
history and in every bundle taken before that commit, and both formats carry the same
`name` / `description` frontmatter — so nothing about a file's shape says which contract it
was written against. `kind: improve` does, and it is required:

  - a prompt.md without the marker is refused at startup. Loading one would paste "swap
    when the gap exceeds 5bps" into the reviser's brief under "the operator's
    instructions", and nothing anywhere would say so
  - a directory still carrying improve.md is also refused, rather than silently running a
    strategy nothing ever revises — the quiet degradation this repo has been removing
    since #44
  - both checks happen before the first block is traded. Loading the policy inside the
    improvement loop meant a configuration error surfaced several blocks in, dressed as a
    mid-run crash

Verified: all four agents in config/liquity.yaml resolve to improve mode with no stderr,
and the loader's two refusals are pinned by tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uestion measured (#39)

Issue #39 left two agents as follow-on and one open point as "measured, not assumed":
whether Liquity's ordering sensitivity needs special handling when the environment
rewrites the oracle every block ahead of every agent. Both were still open because
nothing had ever opened a Trove: across every run of the venue, only two of its eight
actions had executed on chain, no Trove was ever liquidated, and the Stability Pool had
never paid out.

**trove-manager** holds a Trove through a price path, against the three ways the venue can
take it: liquidation under 110%, redemption against the riskiest Trove in the list, and
Recovery Mode moving everyone's floor to 150% at once. Whether any of those defences work
turns on one switch — what the borrower did with the eUSD it drew. Held, it is the
repayment that raises a failing ratio for free; sold, the only defence left is collateral,
and a borrower who posted all of it has none. Both are shipped, differing in exactly that.

**sp-underwriter** takes the other side: deposit eUSD, call the liquidation nobody else
has (it is permissionless and pays the caller), and sell the collateral it receives rather
than carry a price bet it never chose.

**The measurement.** The Trove crossed MCR at block 982; the agent, reading a one-block-old
observation, saw it at 983; the liquidation landed at 984. Two blocks, of which one is the
observation lag and one is the mempool — the same as every other venue here. And unlike a
partial redemption, `liquidate()` carries no value that must still match at execution: it
re-derives the ratio from whatever price it executes at. A price that recovers in those two
blocks costs the caller a reverted transaction, not a broken path. So the answer to the open
point is that the sensitivity is real and needs no special handling; the redemption helper
has no counterpart here.

What the run also settled, from ground truth rather than inference (`liquity_liquidation`
is now read from TroveManager's own logs):

  - all eight actions have now executed on chain, across the two regimes, with no reverts
  - the defenceless borrower lost 20 ETH of collateral and kept ~48k of USDC: -13,140,
    which is the price move plus fees and exactly what a CDP is supposed to cost
  - the underwriter made +4,580 on the collateral it absorbed
  - LQTY behaved as designed: it accrued to a Stability Pool depositor and was reported as
    an unaccounted holding rather than silently valued at zero

**Recovery Mode remains unreachable** and the regime says so rather than implying otherwise:
the 300% genesis Trove dominates the system TCR, which stays near 2.7 through a 22% crash.
Reaching it means recalibrating the genesis position, which changes the venue for every
other regime — a decision worth taking on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…150%

Every description of it I wrote said that below a 150% system TCR "every Trove under 150%
becomes liquidatable at once". Reading TroveManager again: the branch is
`_ICR >= MCR && _ICR < _TCR && entireTroveDebt <= LUSDInSPForOffsets`, so the line is the
*current* TCR — which is already under 150% whenever Recovery Mode is on — and the
liquidation only goes through when the Stability Pool can absorb that Trove's whole debt.
What is seized is capped at 110% of the debt and the remainder becomes a claimable
collateral surplus.

That matters to both reference agents. A borrower reading the old wording defends a line
that is higher than the real one, and an underwriter expects Recovery Mode liquidations to
be available whenever a Trove is under 150% when they are gated on pool depth. Neither
agent's behaviour was wrong — the borrower targets CCR with a margin and the underwriter
only liquidates under MCR, both conservative under either reading — but the text they and
an LLM revising them read was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regime header and CLAUDE.md both said reaching it "means changing the genesis
calibration", which is true and incomplete: every route to it multiplies system debt, and
that dilutes the redemption fee curve and the Stability Pool's relative depth — the two
properties #39 measured. Both notes now carry the measured numbers and the issue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The question in #37 was simply whether copying and modifying an agent directory is
allowed, and the honest answer was that nobody could tell: there was no LICENSE file, no
`license` field in any of the five package.json files, and nothing in the README. An
unlicensed public repository grants nothing, which is the opposite of what a competition
that tells participants to copy `example/agents/<id>/` needs.

MIT, because the workflow the repository is built around is "take this directory and make
it yours", and anything with conditions attached would have to be explained on every page
that says so.

#39 treats this as a hard blocker on the grounds that "Liquity V1 is GPL-3.0". That premise
does not survive contact with the source: all 81 of its contract files carry
`SPDX-License-Identifier: MIT`, and `vendor/liquity-src/` is gitignored — setup-vendors.sh
clones it at a pinned commit on the machine that builds, so this repository never
redistributes a line of it. The dependency creates no obligation either way.

What it does not do is make everything in the tree MIT, and THIRD-PARTY.md says so rather
than letting the LICENSE imply it. The canonical WETH9 mock is GPL-3.0 and keeps its own
header — it is vendored verbatim precisely so the local chain's wrapped ether behaves like
the one every venue integrates against — and Curve's prebuilt artifacts are committed
because rebuilding Vyper reproducibly needs a pinned toolchain. Both are recorded with
their upstreams.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adachi-440
adachi-440 merged commit 6129ad8 into main Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sim] CDP stablecoin venue: fork Liquity V1 core (Troves, Stability Pool, redemption arb) Clarify the repository license

1 participant