harb/onchain/script/backtesting/attacks/SCHEMA.md
johba ce9be22d2e 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) <noreply@anthropic.com>
2026-03-22 10:53:07 +00:00

198 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.