Merge pull request 'fix: Attack file schema for burn_lp needs documentation and migration (#615)' (#1111) from fix/issue-615 into master
This commit is contained in:
commit
1b4de1c081
9 changed files with 214 additions and 0 deletions
|
|
@ -228,6 +228,11 @@ contract AttackRunner is Script {
|
||||||
_seq = 1;
|
_seq = 1;
|
||||||
string memory line = vm.readLine(attackFile);
|
string memory line = vm.readLine(attackFile);
|
||||||
while (bytes(line).length > 0) {
|
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);
|
bool isRecenter = _execute(line);
|
||||||
if (isRecenter) {
|
if (isRecenter) {
|
||||||
_logSnapshot(_seq++);
|
_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":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// schema-version: 1
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
// schema-version: 1
|
||||||
{"op":"buy_recenter_loop","count":80,"amount":"100000000000000000000"}
|
{"op":"buy_recenter_loop","count":80,"amount":"100000000000000000000"}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// schema-version: 1
|
||||||
{"op":"buy","amount":"31900000000000000000"}
|
{"op":"buy","amount":"31900000000000000000"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// schema-version: 1
|
||||||
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"10000000000000000000","token":"WETH"}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// schema-version: 1
|
||||||
{"op":"buy","amount":"50000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"50000000000000000000","token":"WETH"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
||||||
|
|
|
||||||
|
|
@ -446,6 +446,11 @@ contract FitnessEvaluator is Test {
|
||||||
|
|
||||||
string memory line = vm.readLine(attackFile);
|
string memory line = vm.readLine(attackFile);
|
||||||
while (bytes(line).length > 0) {
|
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);
|
_executeOp(line);
|
||||||
line = vm.readLine(attackFile);
|
line = vm.readLine(attackFile);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue