Liquid staking (LST) venue: wstETH-style vault, LST/WETH market, and leverage #9
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
| # Full-chain check: deploy every venue to a bare anvil, dump the state, and prove a backtest runs | |
| # against that dump. | |
| # | |
| # This is the only thing that would have caught the GMX localhost market bug (#42's `1502616`), where | |
| # `npm run deploy` had been dying at GM seeding -- nothing in the fast CI touches the deployer, and | |
| # the state dump is gitignored, so no per-PR job can exercise this path. | |
| # | |
| # ~25-40 minutes, dominated by the gmx-synthetics clone + yarn install and the hardhat deploy, so it | |
| # only runs when something it actually covers changes. | |
| name: deploy + backtest | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "deployer/**" | |
| - "scripts/genStateDump.ts" | |
| - "scripts/genLocalConstants.ts" | |
| - "core/src/cli/backtest.ts" | |
| - "core/src/backtest/**" | |
| - "sdk/src/constants.ts" | |
| - "sdk/src/constants.local.ts" | |
| - "config/regimes/**" | |
| # Scoring. A run's value series cannot be regression-tested any other way: tx timing and | |
| # ordering are non-deterministic (ADR 0005), so a golden run does not exist and unit tests can | |
| # only pin individual functions. This job is the only thing that puts the scorer in front of | |
| # real venue state across all five venues. #44 changed the scorer and skipped this job. | |
| - "core/src/realtime/reconstruct.ts" | |
| - "sdk/src/valuation.ts" | |
| - "sdk/src/protocols/**" | |
| - ".github/workflows/deploy-backtest.yml" | |
| concurrency: | |
| group: deploy-backtest-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| deploy-and-backtest: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - uses: foundry-rs/foundry-toolchain@v1 | |
| with: | |
| version: stable | |
| - run: npm ci | |
| # The mock oracles and PriceFeed the run deploys at setup. The deployer's own `forge build` | |
| # writes to deployer/out and does not cover these. | |
| - run: npm run build:contracts | |
| - name: Set up the deployer | |
| working-directory: deployer | |
| run: | | |
| npm install | |
| forge build | |
| cp .env.example .env | |
| # anvil is started by this workflow, not by the deployer, so gen:state-dump can talk to the | |
| # same chain after the deploy process has exited. | |
| sed -i 's/^MANAGE_ANVIL=.*/MANAGE_ANVIL=false/' .env | |
| ./scripts/setup-vendors.sh | |
| - name: Start anvil | |
| working-directory: deployer | |
| run: | | |
| npm run anvil > "$RUNNER_TEMP/anvil.log" 2>&1 & | |
| for _ in $(seq 1 60); do | |
| cast block-number --rpc-url http://127.0.0.1:8545 >/dev/null 2>&1 && exit 0 | |
| sleep 2 | |
| done | |
| echo "anvil did not come up"; cat "$RUNNER_TEMP/anvil.log"; exit 1 | |
| - name: Deploy all venues | |
| working-directory: deployer | |
| run: npm run deploy -- --keep-fresh | |
| - name: Generate constants and the state dump | |
| run: | | |
| npm run gen:local-constants | |
| npm run gen:state-dump | |
| - name: Backtest against the dump | |
| run: npm run backtest -- --regime calm-01 --blocks 12 | |
| # The backtest exits 0 even when the scorer quietly read nothing, which is the failure mode | |
| # this whole job exists to catch, so assert on the run's own output. | |
| - name: Check the run is healthy | |
| run: | | |
| node -e ' | |
| const { readdirSync, readFileSync } = require("node:fs"); | |
| const runs = readdirSync("runs").filter((d) => /^\d{4}-/.test(d)).sort(); | |
| const id = runs.at(-1); | |
| if (!id) throw new Error("no run directory was produced"); | |
| const s = JSON.parse(readFileSync(`runs/${id}/summary.json`, "utf8")); | |
| const problems = []; | |
| if (s.valueSeries?.failedReads !== 0) | |
| problems.push(`failedReads=${s.valueSeries?.failedReads}`); | |
| if ((s.violations ?? []).length) problems.push(`violations=${JSON.stringify(s.violations)}`); | |
| if (!s.agents?.length) problems.push("no agents were scored"); | |
| for (const a of s.agents ?? []) { | |
| if (!Number.isFinite(a.finalValueUsdc) || a.finalValueUsdc <= 0) | |
| problems.push(`${a.id} finalValueUsdc=${a.finalValueUsdc}`); | |
| } | |
| const unpriced = s.valueSeries?.unpricedHoldings ?? []; | |
| if (unpriced.length) console.log(`note: ${unpriced.length} unpriced holding(s):`, JSON.stringify(unpriced)); | |
| if (problems.length) throw new Error(`unhealthy run ${id}: ${problems.join(", ")}`); | |
| console.log(`run ${id} ok: ${s.agents.length} agents, ${s.blocksProcessed} blocks, failedReads 0`); | |
| ' | |
| - name: Upload the run and deploy logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: deploy-backtest-output | |
| path: | | |
| runs/ | |
| deployer/deployments/deployments.json | |
| backtest/state/manifest.json | |
| ${{ runner.temp }}/anvil.log | |
| retention-days: 7 |