Skip to content

Commit 6fb90ca

Browse files
authored
Merge pull request #51 from NyxFoundation/feat/lst-venue
Liquid staking (LST) venue: wstETH-style vault, LST/WETH market, and leverage
2 parents 505d6fc + 6374386 commit 6fb90ca

47 files changed

Lines changed: 6276 additions & 110 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ config/*.yaml
99
!config/all18-mixed.yaml
1010
!config/claude-llm.yaml
1111
!config/vuln-test.yaml
12+
!config/lst.yaml
1213
runs/
1314
cache/
1415
out/

CLAUDE.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ validate 失敗はエラー内容を会話に追記して再試行(上限超
4444

4545
run の設定値とエージェントロスターは **`config/local.yaml` 一本**で管理する(env からの設定読取は廃止)。
4646
解決順は `--config <path>` > `ERIS_CONFIG` > `config/local.yaml` > `config/example.yaml`(committed 雛形 = zero-config 既定)。
47+
**雛形は `run.localDeploy: true` 既定**(README Quick Start と config/regimes/* に揃えた。fork 用フラグは不要になり
48+
`npm run sim:realtime` だけで走る)。fork に戻すには `localDeploy: false` + `run.protocols` から `lst` を外す
49+
(LST の vault は自作で Arbitrum に対応物が無い)+ `ARB_RPC_URL` + 別端末で `npm run anvil`
4750
キーは**ネスト lowercase**`run` / `funding` / `limits` / `flow` / `stress` / `vuln` + `agents`)で
4851
`sdk/src/runConfig.ts``SCHEMA` が内部キーへ写す。ロスターは規約解決(ADR 0015 §6):
4952

@@ -89,6 +92,59 @@ OU の base price はそのまま進め、その上に **SEED 由来でランダ
8992
- stress run(events かつ `ERIS_RUN_BLOCKS>0`)は**時間制限を自動無効化**しブロック数で終了する(`ERIS_RUN_SECONDS` が先に切れて crash 窓へ到達しない事故を回避。override は `stress_run_time_limit_disabled` で記録)
9093
- coordinator は `stress_schedule` / `stress_victim_hf` / `stress_liquidation` を events.jsonl へ emit する。liquidator agent には victim アドレスを `ERIS_LIQUIDATION_VICTIMS` で配布する。清算の帰属は agent ログの `liquidationCall`(rawTx) を一次情報にする(events.jsonl を直接読んで解析する。旧 stress-report ツールは撤去済み)
9194

95+
### LST venue(wstETH 風 vault + LST/WETH 二次市場。issue #38 Phase 1。既定 off・**ローカルデプロイ専用**)
96+
97+
利回りで償還レートが上がる非 rebasing の LST(`deployer/contracts/MockLSTVault.sol`)と、その二次市場
98+
(既存 stableswap-ng factory 上の LST/WETH plain pool)。**同じ資産に価格が 2 つある**のが本質:
99+
`redemptionRateWeth`(vault が負う par。ただし出金キュー `withdrawalDelayBlocks` 待ち)と
100+
`marketPriceWeth`(プールが今払う額。discount 付き)。observation は両方 + `discountBps` /
101+
`yieldPerBlockBps` / キュー長 / 自分サイズでの `instantExitWethWei` / pending を別々に出す。
102+
103+
- **Arbitrum に対応物が無い**(vault は自作)ので fork では使えない。`run.protocols` に `lst` を入れて
104+
ローカルデプロイでないと起動時 fail-fast。**`config/example.yaml` の既定ロスターに入っている**
105+
(`cd deployer && npm run deploy -- --keep-fresh` → `npm run gen:local-constants` → `npm run sim:realtime`)。
106+
LST 単独で見たいときは競合参加者と較正ノブを明示した `config/lst.yaml`
107+
- **利回りは EVM 時間でなく経済クロック**(`lst.simulatedSecondsPerBlock` / `lst.apyBps`。既定 1 block=1h・3%/yr
108+
= Aave WETH supply と同オーダー。速すぎると他 venue が無意味になる)。原資は事前投入 reward reserve に上限され、
109+
`accrueRewards()` は permissionless(額はブロック数の純関数なので誰が叩いても同じ)。coordinator は毎ブロック
110+
oracle tx と**同じ admin nonce の直列**で叩く(並列にすると nonce 衝突でレートが凍る)
111+
- **プールの rate oracle 配線が要**(`stEthPerToken()` を asset_type=1 で登録)。未配線だとレート上昇が全員に開かれた
112+
無リスク裁定になる(ADR 0007 を毀損)。deploy 時 assert + 起動時 `lst_setup` で乖離 200bps 超は fail-fast
113+
- **採点は realizable**(`sdk/src/protocols/lst.ts` `realizableWethWei`): shares は「今プールで売った額」と
114+
「run 終了までに finalize するキューの par」の**良い方**。run 終了後にしか claim できない pending は価値から外し
115+
`reason:"unrealizable"` で `scoring_unpriced_holdings` に報告する(黙って 0 にしない)。#41 の staged-read
116+
インターフェース(`valueAtBlock` / `liquidatableValueUsdc` / `ValuationContext.horizonBlock`)の最初の消費者
117+
- **Phase 2(選択を非自明にする)実装済み**。`config/lst.yaml` の `lst:` / `stress:` に較正例:
118+
- **APY 変動** — `lst.apyRangeBps` + `apyStepBlocks` で seed 由来 Rng(独立 salt)から N ブロックごとに再サンプル
119+
→ coordinator が `setRewardRate`。固定利回りだと「block 0 で全ステーク」が恒久最適になるため
120+
- **キュー混雑 + サイズ依存** — vault の finalize をスループット律速に(`queueThroughputWeiPerBlock`)。
121+
`claimableAt = max(floor, queueDrainBlock) + ceil(assets/throughput)` = 大口ほど待ち、先客がいるほど待つ。
122+
観測は実効待ちを `estimatedQueueDelayBlocks`(自分の全保有)と `queueDelayPerWethBlocks`(限界 1 WETH)で分けて出す。
123+
**採点も実効待ちを使う**(floor で判定すると完了不能な exit を par 評価してしまう)
124+
- **`lstSlash`** — ADR 0009 と同じレンジ config で `stress.events` に書ける点イベント。1 ブロックで rate を恒久的に下げる。
125+
**discount は開かない**(プールが rate oracle 追随でリプライスする=oracle が正しく効いている証拠)。
126+
slash は「保有者が損をする」リスクであって裁定機会ではない。よって magnitude は利回りスケールで較正する
127+
(70 ブロック run の利回り ~3-8bps に対し 10-30bps。最初に試した 100-300bps は利回りの 15 倍でステーク自体が常に負けになった)
128+
- **Phase 3(レバレッジ)実装済み**。`run.protocols` に `aave` を足すと有効:
129+
- deployer が LST を **Aave の担保専用 reserve** として登録(`registerLstReserve`。LTV 70% / LT 75% /
130+
bonus 7.5%。**borrow は無効**=現実の LST 上場と同じで、狙いは「LST を担保に ETH を借りる」レバステーキング)。
131+
Aave 自身の同名 reserve から clone できないため **LTV/LT は明示指定**(issue #38 が指摘した通り)。
132+
rate strategy のみ WETH から借用
133+
- **価格は WETH × 償還レート**。専用 MockAggregator を持ち、`sdk/src/protocols/oracles.ts` が
134+
他の全オラクルと同じ 3 経路(mined / mempool / storage)で毎ブロック書く。よって **1 ブロック遅れ**を継承し、
135+
slash はまず vault に効き、次ブロックで HF に届く = liquidation cascade の起点
136+
- `aaveSupply`/`aaveWithdraw` の asset に `"LST"` を指定可能(`TokenKind` に `"lst"` を追加し、
137+
scorer の spot 掃引から外して二重計上を防いでいる。評価は Aave の totalCollateralBase 経由)
138+
- `lst-carry` は **`ERIS_LST_LEVERAGE_TARGET_HF` で opt-in**(既定 0=off)。ループは
139+
stake→collateralize→borrow→stake で、目標 HF に**着地する**サイズだけ借りる(headroom 基準で借りると
140+
目標も下限も突き抜けて borrow/repay が振動する: 実測 24/22 → 2/0)。HF が下限を割ったら他の何より先に返済。
141+
prompt 版は spot に専念(LLM に env の opt-in は効かないため、手を出さないよう明記)
142+
- 市場側の検証は `test/lstLeverage.test.ts`(要 `ERIS_LOCAL_DEPLOY=1` + ローカルデプロイ。実チェーンで
143+
listing → ETH 借入 → slash 後も HF 不変(=oracle lag) → oracle 更新で HF 低下 を検査。CI では skip)
144+
- **USDC 建て採点では LST 保有戦略は構造的に β で不利**(実測: noop 0 > lst-carry −203 > lst-carry-wide −233、
145+
一方で WETH を持たない venue-arb は +115)。alphaUsdc は free inventory の β しか除去せず、
146+
LST ポジションは live mark のため。ETH 建て採点(DAT 型)が issue #38 の motivation で follow-on
147+
92148
実時間化(ADR 0005)の前提: **SEED(=regime) は市場条件のラベル**で価格パスは再現可能だが、tx タイミング/着順は非決定 → 同一 regime でも結果はぶれる。run 長は `ERIS_RUN_BLOCKS` 固定で揃える。run の比較が要るときは同一 config を複数回回してサンプルを貯め、`runs/<id>/summary.json` を集計する(旧 evaluate/gate は撤去済み)。
93149

94150
## アーキテクチャ(環境とエージェント実行の分離。ADR 0006 / ADR 0015)

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ flowchart LR
4141

4242
## What is this
4343

44-
- **Multi-protocol DeFi environment** — Uniswap V3 / Balancer v2 / Curve / Aave v3 / GMX v2 are all provisioned on a single Anvil, and enabled pluggably through the protocol adapter registry (`sdk/src/protocols/`).
44+
- **Multi-protocol DeFi environment** — Uniswap V3 / Balancer v2 / Curve / Aave v3 / GMX v2, plus a liquid-staking venue (a wstETH-style vault and its LST/WETH market), are all provisioned on a single Anvil and enabled pluggably through the protocol adapter registry (`sdk/src/protocols/`).
4545
- **Multi-agent competition** — agents run as fully independent processes, subscribe to blocks at their own pace, and sign and send directly themselves. In-block ordering is determined by anvil `--order fees` (descending priority fee).
4646
- **Controllable fair price** — the coordinator generates a SEED-derived deterministic fair price every block and writes it to the on-chain `PriceFeed` and mock oracles. Aave health factors and GMX mark prices follow it.
4747
- **Market stress & liquidation** — price spikes/crashes can be injected to trigger the Aave liquidation path.
@@ -93,15 +93,17 @@ To skip LLMs entirely and run the same strategies rule-based (`agent.ts`), remov
9393
# Separate terminal: start anvil + deploy all venues via deployer (do not pass --exit)
9494
cd deployer && npm run deploy -- --keep-fresh
9595

96-
# poc side (repository root): import the deploy addresses and run in local deploy mode
96+
# poc side (repository root): import the deploy addresses and run
9797
npm run gen:local-constants
98-
npm run sim:realtime -- --local-deploy \
99-
--seed 1 --blocks 100 --seconds 300 --protocols uniswap,balancer,curve
100-
# The roster is the inline agents in config/local.yaml (edit the YAML to swap it out.
101-
# backtest supports swapping via --agents <roster.yaml>)
98+
npm run sim:realtime
99+
# The roster and every run knob come from config/local.yaml (edit the YAML to swap them out;
100+
# backtest supports swapping the roster via --agents <roster.yaml>). One-off overrides are CLI
101+
# flags: npm run sim:realtime -- --seed 2 --blocks 40
102102
```
103103

104-
> The `--local-deploy` flag (or config `run.localDeploy: true`) switches to local deploy mode. The CLI entry point detects this at startup, sets `ERIS_LOCAL_DEPLOY=1` internally, and `sdk/src/constants.ts` overlays the locally-deployed addresses (WETH/USDC/WBTC, etc.) — no need to pass the env by hand.
104+
> `config/example.yaml` ships with `run.localDeploy: true`, so no flag is needed. The CLI entry point detects it at startup, sets `ERIS_LOCAL_DEPLOY=1` internally, and `sdk/src/constants.ts` overlays the locally-deployed addresses (WETH/USDC/WBTC, etc.) — no need to pass the env by hand. `--local-deploy` still works as a one-off override for a config that does not set it.
105+
106+
> To run against an Arbitrum fork instead, set `run.localDeploy: false` in `config/local.yaml`, remove `lst` from `run.protocols` (its vault is deployed by us and has no Arbitrum counterpart), put `ARB_RPC_URL` in `.env.local`, and start `npm run anvil` in another terminal.
105107
106108
> LLM decisions take ~10s each, hence the 100-block / 300s run above (rule-based runs are fine with 24 blocks / 70s). If the trading agents only emit `noop`, you probably skipped [Choose an LLM backend](#choose-an-llm-backend) — check `runs/<run_id>/agents/<id>.jsonl` for `llm cycle skipped`.
107109

config/example.yaml

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
# `.env.local` (ARB_RPC_URL / *_PRIVATE_KEY / ANTHROPIC_API_KEY / OLLAMA_API_KEY).
1313
# The local Anvil dev keys are used by default even when unspecified.
1414
#
15+
# Local deploy by default (run.localDeploy: true), matching the README Quick Start and the official
16+
# backtest regimes: every venue is deployed onto a bare anvil by the bundled deployer/, so no fork
17+
# RPC is involved. Start it with `cd deployer && npm run deploy -- --keep-fresh`, then
18+
# `npm run gen:local-constants` to import the addresses. To run against an Arbitrum fork instead,
19+
# set localDeploy: false, drop `lst` from run.protocols (its vault has no Arbitrum counterpart) and
20+
# start `npm run anvil` in another terminal with ARB_RPC_URL set.
21+
#
1522
# LLM-driven by default: the trading agents below run in prompt mode (prompt.md, one LLM call per
1623
# decision; ERIS_AGENT_MODE: "prompt") and need an LLM backend. Pick one:
1724
# - Ollama Cloud (default; model gpt-oss:120b): set OLLAMA_API_KEY in `.env.local`
@@ -29,13 +36,20 @@ run:
2936
blocks: 100
3037
seconds: 300
3138
blockTimeSec: 2
32-
protocols: [uniswap, balancer, curve]
39+
# gmx and aave are left out because they are the slow half of the deploy; add them once
40+
# `deployer` has run with them. `lst` is local-only (issue #38) and fails fast on a fork.
41+
protocols: [uniswap, balancer, curve, lst]
3342
economicGas: false
34-
localDeploy: false
43+
localDeploy: true
3544
reportDir: ./runs
3645

3746
funding:
38-
wethWei: "0" # USDC-only distribution (removes initial directional exposure)
47+
# WETH is handed out because the LST venue is WETH-denominated and cannot be traded without it.
48+
# That reintroduces price drift (beta) into netPnlUsdc, which is fine for a template you run to
49+
# see the thing work — read alphaUsdc for skill. The official regimes under config/regimes/ stay
50+
# USDC-only (wethWei: "0") so evaluation runs keep their clean alpha; drop this to "0" too if you
51+
# are measuring rather than exploring, and take `lst` out of run.protocols with it.
52+
wethWei: "20000000000000000000" # 20 WETH
3953
usdcUnits: "25000000000"
4054
# base: { WBTC: "0" } # initial inventory of additional bases (default 0)
4155

@@ -103,3 +117,10 @@ agents:
103117
description: base-agnostic cross-venue arbitrage (all active bases x all venues; LLM-driven via prompt.md)
104118
env: { ERIS_AGENT_MODE: "prompt", ERIS_PROMPT_LOG_CALLS: "1" }
105119
# env: { ERIS_AGENT_MODE: "prompt", ERIS_PROMPT_LOG_CALLS: "1", ERIS_LLM_MODEL: "claude-cli:haiku" }
120+
- id: lst-carry
121+
wallet: AGENT4_PRIVATE_KEY
122+
description: liquid staking — stake for yield, or trade the LST redemption/market gap (LLM-driven via prompt.md)
123+
env: { ERIS_AGENT_MODE: "prompt", ERIS_PROMPT_LOG_CALLS: "1" }
124+
# env: { ERIS_AGENT_MODE: "prompt", ERIS_PROMPT_LOG_CALLS: "1", ERIS_LLM_MODEL: "claude-cli:haiku" }
125+
# config/lst.yaml is the same venue with a second competing participant and the calibration knobs
126+
# spelled out, if you want to look at the LST market on its own.

0 commit comments

Comments
 (0)