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>
This commit is contained in:
parent
e22e6ac7bb
commit
ce9be22d2e
9 changed files with 214 additions and 0 deletions
|
|
@ -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++);
|
||||
|
|
|
|||
198
onchain/script/backtesting/attacks/SCHEMA.md
Normal file
198
onchain/script/backtesting/attacks/SCHEMA.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// schema-version: 1
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// schema-version: 1
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
// schema-version: 1
|
||||
{"op":"buy_recenter_loop","count":80,"amount":"100000000000000000000"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// schema-version: 1
|
||||
{"op":"buy","amount":"31900000000000000000"}
|
||||
{"op":"recenter"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// schema-version: 1
|
||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||
{"op":"sell","amount":"all","token":"KRK"}
|
||||
{"op":"recenter"}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// schema-version: 1
|
||||
{"op":"buy","amount":"50000000000000000000","token":"WETH"}
|
||||
{"op":"recenter"}
|
||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue