feat: Liquity V1 as the eUSD CDP venue (#39) - #60
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
sdk/src/protocols/liquity.ts—readState/observe/buildTxs/valueUsdc/valueAtBlock, and eight actionsredemption-arb(the venue's α),trove-manager(the borrower),sp-underwriter(the Stability Pool)liquity(redemption arb) andliquity-crash(borrower / underwriter)eusdDepegstress eventeUSD 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_dxbecause 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.
LiquityRedemptionHelpercomputes them afterfetchPrice()hascached 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 matchat 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.
eusdDepegis the stressevent 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
liquitySwapEusdthe venue's own α is unreachable exceptthrough hand-rolled
rawTx.builds it with Foundry in 6.3s, rather than committing prebuilt artifacts.
Ais 100, not the USDC/DAI pool's 2000 — see the measurement.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.
prompt.md, not both files. The issue asks the reference agent to carryagent.tsandprompt.mdbecause 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 thename collision with a required
kind: improvemarker, so an old per-decision prompt is refusedrather than reinterpreted as a policy for revising strategies.
Also here
MIT (closes #37). There was no LICENSE file, no
licensefield in any of the fivepackage.jsonfiles 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.mdrecords 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: MITand
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-managerandsp-underwritershipped despite being listed as follow-on, because without aTrove in existence six of the eight actions had never executed on chain and the ordering question
could not be answered at all.
Verification
check:strategy,check:boundaries— all greenopenTrove12 ·adjustTrove24 ·closeTrove2 ·redeem51 ·provideToSP49 ·withdrawFromSP3 ·liquidate3 ·swapEusd69liquityregime: the full loop — buy 1,822 eUSD at 120bps, redeem, wrap, sell — with theredemption fee moving 50 → 158bps behind it. redemption-arb +57.81 against a noop baseline
liquity-crashregime: a real liquidation (ground truth fromTroveLiquidated). The defencelessborrower 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
npm run backtest; the state dump was regenerated with the venue🤖 Generated with Claude Code