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

6.2 KiB
Raw Blame History

Attack File Schema (v1)

Attack files use JSON Lines 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:

// 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.
{"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.
{"op":"sell","amount":"all","token":"KRK"}

recenter

Call LiquidityManager.recenter() via the authorized recenter account. No additional fields. Emits a snapshot in AttackRunner.

{"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
{"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()
{"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
{"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
{"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:

{"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
{"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.