From ce9be22d2e3aac536a39aea30744a59a60380361 Mon Sep 17 00:00:00 2001 From: johba Date: Sun, 22 Mar 2026 10:53:07 +0000 Subject: [PATCH 1/2] fix: Attack file schema for burn_lp needs documentation and migration (#615) Add SCHEMA.md documenting the JSONL attack file format with all operation definitions, field types, and the burn_lp tokenId convention divergence between AttackRunner (.positionIndex) and FitnessEvaluator (.tokenId). Add schema-version header comments to all existing attack files and teach both consumers to skip comment lines starting with //. Co-Authored-By: Claude Opus 4.6 (1M context) --- onchain/script/backtesting/AttackRunner.s.sol | 5 + onchain/script/backtesting/attacks/SCHEMA.md | 198 ++++++++++++++++++ .../attacks/fee-drain-oscillation.jsonl | 1 + .../attacks/il-crystallization-15.jsonl | 1 + .../attacks/il-crystallization-80.jsonl | 1 + .../attacks/il-crystallization-optimal.jsonl | 1 + .../backtesting/attacks/round-trip-safe.jsonl | 1 + .../backtesting/attacks/staking-safe.jsonl | 1 + onchain/test/FitnessEvaluator.t.sol | 5 + 9 files changed, 214 insertions(+) create mode 100644 onchain/script/backtesting/attacks/SCHEMA.md diff --git a/onchain/script/backtesting/AttackRunner.s.sol b/onchain/script/backtesting/AttackRunner.s.sol index 4cd0c31..7ba1420 100644 --- a/onchain/script/backtesting/AttackRunner.s.sol +++ b/onchain/script/backtesting/AttackRunner.s.sol @@ -228,6 +228,11 @@ contract AttackRunner is Script { _seq = 1; string memory line = vm.readLine(attackFile); while (bytes(line).length > 0) { + // Skip comment lines (e.g. "// schema-version: 1" header). + if (bytes(line).length >= 2 && bytes(line)[0] == 0x2F && bytes(line)[1] == 0x2F) { + line = vm.readLine(attackFile); + continue; + } bool isRecenter = _execute(line); if (isRecenter) { _logSnapshot(_seq++); diff --git a/onchain/script/backtesting/attacks/SCHEMA.md b/onchain/script/backtesting/attacks/SCHEMA.md new file mode 100644 index 0000000..e38d541 --- /dev/null +++ b/onchain/script/backtesting/attacks/SCHEMA.md @@ -0,0 +1,198 @@ +# Attack File Schema (v1) + +Attack files use [JSON Lines](https://jsonlines.org/) format (`.jsonl`): one JSON +object per line, each representing a single operation executed sequentially against +a forked chain. + +## Schema version + +Schema version is tracked in this document and referenced by a comment line at the +top of each attack file: + +```jsonl +// schema-version: 1 +``` + +JSONL parsers that do not tolerate comment lines should skip lines starting with `//`. + +## Consumers + +Two independent consumers execute attack files: + +| Consumer | Location | Context | +|----------|----------|---------| +| **AttackRunner** | `onchain/script/backtesting/AttackRunner.s.sol` | Foundry script (`forge script`), runs against a live Anvil fork | +| **FitnessEvaluator** | `onchain/test/FitnessEvaluator.t.sol` | Foundry test (`forge test`), runs in-process against a fork | + +Both consumers read the same `.jsonl` files from this directory and interpret +operations identically, with one exception noted under `burn_lp` below. + +## Operations + +### `buy` + +Swap WETH → KRK via SwapRouter. + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"buy"` | Operation identifier | +| `amount` | string (wei) | WETH amount to swap | +| `token` | string | Ignored (WETH assumed). Present for readability. | + +```jsonl +{"op":"buy","amount":"100000000000000000000","token":"WETH"} +``` + +### `sell` + +Swap KRK → WETH via SwapRouter. + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"sell"` | Operation identifier | +| `amount` | string (wei) or `"all"` | KRK amount to swap; `"all"` sells entire balance | +| `token` | string | Ignored. Present for readability. | + +```jsonl +{"op":"sell","amount":"all","token":"KRK"} +``` + +### `recenter` + +Call `LiquidityManager.recenter()` via the authorized recenter account. +No additional fields. Emits a snapshot in AttackRunner. + +```jsonl +{"op":"recenter"} +``` + +### `buy_recenter_loop` + +Batch N × (buy → recenter) cycles in a single op. Avoids per-step forge +overhead for high-cycle attacks. + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"buy_recenter_loop"` | Operation identifier | +| `count` | uint | Number of buy→recenter cycles | +| `amount` | string (wei) | WETH amount per buy | + +```jsonl +{"op":"buy_recenter_loop","count":80,"amount":"100000000000000000000"} +``` + +### `stake` + +Call `Stake.snatch()` to create a staking position. + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"stake"` | Operation identifier | +| `amount` | string (wei) | KRK amount to stake | +| `taxRateIndex` | uint | Raw tax-rate value passed to `Stake.snatch()` | + +```jsonl +{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0} +``` + +### `unstake` + +Call `Stake.exitPosition()`. + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"unstake"` | Operation identifier | +| `positionId` | uint | 1-based index into staking positions created by prior `stake` ops in this run | + +```jsonl +{"op":"unstake","positionId":1} +``` + +### `mint_lp` + +Add a Uniswap V3 LP position via the NonfungiblePositionManager (NPM). +The minted NPM tokenId is stored internally (in insertion order) and can +be referenced by subsequent `burn_lp` ops. + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"mint_lp"` | Operation identifier | +| `tickLower` | int | Lower tick boundary | +| `tickUpper` | int | Upper tick boundary | +| `amount0` | string (wei) | token0 amount | +| `amount1` | string (wei) | token1 amount | + +```jsonl +{"op":"mint_lp","tickLower":-100,"tickUpper":100,"amount0":"1000000","amount1":"1000000"} +``` + +### `burn_lp` + +Remove a Uniswap V3 LP position (decreaseLiquidity + collect). + +**tokenId convention:** The position to burn is identified by a **1-based index** +into the list of NPM tokenIds created by prior `mint_lp` ops in this attack run +(index 1 = first `mint_lp`, index 2 = second, etc.). This indirection exists because +raw NPM tokenIds vary depending on the fork block tip and would make attack files +non-portable across fork blocks (see issue #614). + +> **Field-name divergence (consumer-specific):** +> +> | Consumer | JSON field | Semantics | +> |----------|-----------|-----------| +> | **AttackRunner** | `.positionIndex` | 1-based index into `_mintedLpTokenIds` | +> | **FitnessEvaluator** | `.tokenId` | 1-based index into `_mintedNpmTokenIds` | +> +> Both fields carry the same semantic value (1-based mint_lp index), but the +> JSON key differs. Attack files targeting AttackRunner must use `positionIndex`; +> files targeting FitnessEvaluator must use `tokenId`. +> +> **Migration note:** A future unification should standardise on `positionIndex` +> (the more descriptive name) and update FitnessEvaluator to match. Until then, +> include both fields in attack files that need to work with both consumers: +> ```jsonl +> {"op":"burn_lp","positionIndex":1,"tokenId":1} +> ``` + +### `mine` + +Advance the block number (Anvil/VM only). + +| Field | Type | Description | +|-------|------|-------------| +| `op` | `"mine"` | Operation identifier | +| `blocks` | uint | Number of blocks to advance | + +```jsonl +{"op":"mine","blocks":10} +``` + +## Snapshot schema (AttackRunner output) + +AttackRunner emits a JSON-line snapshot to stdout after each `recenter` op: + +| Field | Description | +|-------|-------------| +| `seq` | Sequence counter | +| `tick` | Current pool tick | +| `lm_eth_free` | Free ETH in LiquidityManager | +| `lm_weth_free` | Free WETH in LiquidityManager | +| `lm_eth_total` | Total ETH in LiquidityManager | +| `positions` | Object with `floor`, `anchor`, `discovery` sub-objects | +| `vwap_x96` | VWAP in X96 format | +| `vwap_tick` | VWAP as tick | +| `outstanding_supply` | Outstanding KRK supply | +| `total_supply` | Total KRK supply | +| `optimizer_output` | Current optimizer parameters | +| `adversary_eth` | Adversary ETH balance | +| `adversary_krk` | Adversary KRK balance | + +## Regenerating attack files + +When regenerating attack files (e.g. via the red-team agent): + +1. Always include the `// schema-version: 1` header as the first line. +2. For `burn_lp` ops, use 1-based `mint_lp` indexes — never raw NPM tokenIds. +3. If the file must work with both AttackRunner and FitnessEvaluator, include + both `positionIndex` and `tokenId` fields with the same value. +4. Test against both consumers after regeneration to catch field-name mismatches. diff --git a/onchain/script/backtesting/attacks/fee-drain-oscillation.jsonl b/onchain/script/backtesting/attacks/fee-drain-oscillation.jsonl index 2faafa2..376717f 100644 --- a/onchain/script/backtesting/attacks/fee-drain-oscillation.jsonl +++ b/onchain/script/backtesting/attacks/fee-drain-oscillation.jsonl @@ -1,3 +1,4 @@ +// schema-version: 1 {"op":"buy","amount":"100000000000000000000","token":"WETH"} {"op":"recenter"} {"op":"sell","amount":"all","token":"KRK"} diff --git a/onchain/script/backtesting/attacks/il-crystallization-15.jsonl b/onchain/script/backtesting/attacks/il-crystallization-15.jsonl index 9aeeac3..b935249 100644 --- a/onchain/script/backtesting/attacks/il-crystallization-15.jsonl +++ b/onchain/script/backtesting/attacks/il-crystallization-15.jsonl @@ -1,3 +1,4 @@ +// schema-version: 1 {"op":"buy","amount":"100000000000000000000","token":"WETH"} {"op":"recenter"} {"op":"buy","amount":"100000000000000000000","token":"WETH"} diff --git a/onchain/script/backtesting/attacks/il-crystallization-80.jsonl b/onchain/script/backtesting/attacks/il-crystallization-80.jsonl index 53262e1..17a83e0 100644 --- a/onchain/script/backtesting/attacks/il-crystallization-80.jsonl +++ b/onchain/script/backtesting/attacks/il-crystallization-80.jsonl @@ -1,2 +1,3 @@ +// schema-version: 1 {"op":"buy_recenter_loop","count":80,"amount":"100000000000000000000"} {"op":"sell","amount":"all","token":"KRK"} diff --git a/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl b/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl index 0f0660b..29cb26d 100644 --- a/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl +++ b/onchain/script/backtesting/attacks/il-crystallization-optimal.jsonl @@ -1,3 +1,4 @@ +// schema-version: 1 {"op":"buy","amount":"31900000000000000000"} {"op":"recenter"} {"op":"sell","amount":"all","token":"KRK"} diff --git a/onchain/script/backtesting/attacks/round-trip-safe.jsonl b/onchain/script/backtesting/attacks/round-trip-safe.jsonl index 8423be4..ddde337 100644 --- a/onchain/script/backtesting/attacks/round-trip-safe.jsonl +++ b/onchain/script/backtesting/attacks/round-trip-safe.jsonl @@ -1,3 +1,4 @@ +// schema-version: 1 {"op":"buy","amount":"10000000000000000000","token":"WETH"} {"op":"sell","amount":"all","token":"KRK"} {"op":"recenter"} diff --git a/onchain/script/backtesting/attacks/staking-safe.jsonl b/onchain/script/backtesting/attacks/staking-safe.jsonl index fe2f2ff..700495e 100644 --- a/onchain/script/backtesting/attacks/staking-safe.jsonl +++ b/onchain/script/backtesting/attacks/staking-safe.jsonl @@ -1,3 +1,4 @@ +// schema-version: 1 {"op":"buy","amount":"50000000000000000000","token":"WETH"} {"op":"recenter"} {"op":"stake","amount":"1000000000000000000000","taxRateIndex":0} diff --git a/onchain/test/FitnessEvaluator.t.sol b/onchain/test/FitnessEvaluator.t.sol index 2fe0650..15d2a85 100644 --- a/onchain/test/FitnessEvaluator.t.sol +++ b/onchain/test/FitnessEvaluator.t.sol @@ -446,6 +446,11 @@ contract FitnessEvaluator is Test { string memory line = vm.readLine(attackFile); while (bytes(line).length > 0) { + // Skip comment lines (e.g. "// schema-version: 1" header). + if (bytes(line).length >= 2 && bytes(line)[0] == 0x2F && bytes(line)[1] == 0x2F) { + line = vm.readLine(attackFile); + continue; + } _executeOp(line); line = vm.readLine(attackFile); } From 27ff88c31bacef6076d23d2ff38ec7a4f63d0bbc Mon Sep 17 00:00:00 2001 From: johba Date: Sun, 22 Mar 2026 11:16:37 +0000 Subject: [PATCH 2/2] ci: retry pipeline