Make burn_lp ops fork-block-independent by using a 1-based positionIndex
(resolved at runtime from prior mint_lp ops) instead of hardcoded NFT
tokenIds. Mirrors the existing pattern used by unstake/_stakedPositionIds.
Also log a warning when burn_lp encounters zero liquidity instead of
silently becoming a no-op.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make V3_FACTORY injectable via vm.envOr("V3_FACTORY", DEFAULT_V3_FACTORY),
preserving the Base mainnet address as the default for existing fork runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fetch-datasets.sh wrapper that fetches HIGHER/WETH, DEGEN/WETH,
and TOSHI/WETH 30-day event caches via fetch-events.ts; reads
INFURA_API_KEY from env and fails with a helpful error if unset
- Update .gitignore from cache/ (whole dir) to cache/*.jsonl so the
pattern is precise to the generated data files; cache/ is already
covered by the repo-root .gitignore via its own cache/ rule
JSONL cache files are gitignored and must be generated locally by
running ./fetch-datasets.sh with INFURA_API_KEY set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-cycle attack extracts 21.3 ETH (2.13%) from 1000 ETH LM:
buy 31.9 ETH → recenter → sell all KRK
Key finding: thin pre-recenter positions allow massive price impact,
recenter rebuilds deep positions at manipulated price, sell through
deep positions recovers most ETH. IL crystallized during recenter.
This is the optimal single-buy amount — 31.95+ hits max tick,
<31 ETH extracts proportionally less.
- DeployBase.sol: remove broken inline second recenter() (would always
revert with 'recenter cooldown' in same Forge broadcast); replace with
operator instructions to run the new BootstrapVWAPPhase2.s.sol script
at least 60 s after deployment
- BootstrapVWAPPhase2.s.sol: new script for the second VWAP bootstrap
recenter on Base mainnet deployments
- StrategyExecutor.sol: update stale docstring that still described the
removed recenterAccess bypass; reflect permissionless model with vm.warp
- TestBase.sol: remove vestigial recenterCaller parameter from all four
setupEnvironment* functions (parameter was silently ignored after
setRecenterAccess was removed); update all callers across six test files
- bootstrap-common.sh: fix misleading retry recenter in
seed_application_state() — add evm_increaseTime 61 before evm_mine so
the recenter cooldown actually clears and the retry can succeed
All 210 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add inline Basescan URL comment identifying V3_FACTORY as the Uniswap V3
Factory on Base mainnet, consistent with the existing comment style used
for NPM_ADDR in both files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Optimizer: add `is IOptimizer` and mark getLiquidityParams() with
`override`, making the interface conformance explicit at the base level.
OptimizerV3 inherits it transitively via Optimizer.
- OptimizerV3Push3: add `is IOptimizer` and implement getLiquidityParams()
that calls calculateParams() with zeroed inputs, returning bear-mode
defaults (ci=0, anchorShare=0.3e18, anchorWidth=100, discoveryDepth=0.3e18).
Behaviour is identical to the previous try/catch fallback used by
LiquidityManager and the backtesting deployer.
- Update backtesting comments to reflect that getLiquidityParams() now
exists on OptimizerV3Push3 (returns bear defaults via zeroed inputs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace 0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2 (Base Sepolia
NonfungiblePositionManager) with the correct Base mainnet address
0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3 in all four files that
referenced it, and add an inline comment citing the chain and source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix the @return NatSpec for recenter() isUp: the previous description
was wrong for the token0=WETH ordering (claimed tick above center, but
the actual check is currentTick < centerTick when token0isWeth). The
correct invariant is isUp=true ↔ KRK price in ETH rose (buy event /
net ETH inflow), regardless of token ordering.
Also address review nit: StrategyExecutor._logRecenter() now logs
'direction=BOOTSTRAP' instead of 'direction=DOWN' when no anchor
position existed before the recenter (aLiqPre==0), eliminating the
misleading directional label on the first recenter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NatSpec to recenter() documenting that the function always reverts
on failure (never silently returns false), listing all four revert
conditions, and clarifying that both true/false return values represent
a successfully-executed recenter with the value indicating price
direction (up vs down relative to previous anchor centre).
Also fix StrategyExecutor.maybeRecenter() to capture the isUp return
value from lm.recenter() and include it in the log output, making
price direction visible in backtesting replays.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `buy_recenter_loop` batch op to AttackRunner — executes N×(buy→recenter)
cycles in a single Solidity loop, emitting snapshots after each recenter.
Rewrite il-crystallization-80.jsonl from 153 individual JSONL steps to 2 lines
using the new op with count=80, matching the intended attack name. Also corrects
the cycle count from 76 (previous file) to the intended 80.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After a buy→sell round-trip the net price movement is near zero, so
recenter() reverts with "amplitude not reached" and aborts the whole
AttackRunner script.
Wrap the recenter() call in a try/catch so amplitude failures are
caught and logged as a skipped step rather than propagating as a fatal
revert. When recenter is skipped, no state snapshot is emitted and the
attack sequence continues — matching the intended semantics: round-trip
trading should not cause the fitness scorer to crash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stake.nextPositionId starts at 654_321, so attack files cannot use literal
on-chain IDs (e.g. positionId=1 always reverts with PositionNotFound).
Fix AttackRunner to treat the JSONL positionId field as a 1-based index into
the list of positions created by stake ops during the current run:
- Add IStake.snatch returns (uint256) to the interface so the returned ID is
captured.
- Track returned IDs in _stakedPositionIds[] (inserted in creation order).
- _executeUnstake resolves positionId to _stakedPositionIds[positionId-1]
before calling exitPosition, matching the natural "unstake position 1"
semantics in the attack DSL.
KRK approval for Stake was already present in _setup(); no other changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- **Bug**: Fix JSON malformation in _snapshotPositions — closing literal was '"}}}' (three
braces) but only '"}}' is needed (close discovery{} + positions{}). The third brace
prematurely closed the root object, making every snapshot unparseable downstream.
- **Nit**: _executeStake local variable renamed taxRateIndex → taxRate to match the
IStake interface and Stake.sol. JSONL field key '.taxRateIndex' is kept for backward
compatibility with existing attack files; the comment and NatDoc header now say so.
- **Nit**: recenter_is_up now emits JSON null (not false) before the first recenter call,
via a new _hasRecentered flag. Downstream parsers can distinguish "no recenter yet"
from "last recenter moved price down" (false). _hasRecentered is set to true alongside
_lastRecenterIsUp in the recenter handler.
- **Nit**: Added a comment to _logSnapshot explaining that pool.slot0() is a view call
and forge-std finalises broadcast state before executing it, so tick/sqrtPrice are
always post-broadcast accurate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- **Bug**: `_positionEthValue` now sums both the ETH component and the KRK component
(converted to ETH via `FullMath.mulDiv` at current sqrtPriceX96) so `lm_eth_total`
correctly reflects LM TVL for all price ranges (below/in/above range).
- **Bug**: `recenter()` return value (`bool isUp` — price direction) is now captured in
`_lastRecenterIsUp` state variable and emitted as `"recenter_is_up"` in every snapshot.
Note: `recenter()` reverts on failure; `false` means price moved *down*, not a no-op.
- **Bug**: Discovery position now emits `"ethValue"` in its snapshot JSON object,
matching the floor and anchor fields for symmetric automated parsing.
- **Warning**: `IStake.snatch` interface parameter renamed `taxRateIndex` → `taxRate` to
match the actual `Stake.sol` signature (the value is a raw rate, not a lookup index).
- **Warning**: Unknown op codes in the JSONL file now emit a `console.log` warning
instead of silently skipping, catching typos in attack sequences.
- **Nit**: `_setup()` now wraps 9 000 ETH (up from 1 000) to cover heavy buy sequences
that would otherwise exhaust the adversary's WETH.
- **Nit**: `_computeVwapTick` documents the int128 overflow guard and its tick=0 sentinel
meaning so callers can distinguish "VWAP unavailable" from tick zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add AttackRunner.s.sol: structured forge script that reads attack ops from a
JSONL file (ATTACK_FILE env), executes them against the local Anvil deployment,
and emits full state snapshots (tick, positions, VWAP, optimizer output,
adversary balances) as JSON lines after every recenter and at start/end.
- Add 5 canonical attack files in onchain/script/backtesting/attacks/:
* il-crystallization-15.jsonl — 15 buy-recenter cycles + sell (extraction)
* il-crystallization-80.jsonl — 80 buy-recenter cycles + sell (extraction)
* fee-drain-oscillation.jsonl — buy-recenter-sell-recenter oscillation
* round-trip-safe.jsonl — 20 full round-trips (regression: safe)
* staking-safe.jsonl — staking manipulation (regression: safe)
- Add scripts/harb-evaluator/export-attacks.py: parses red-team-stream.jsonl
for tool_use Bash blocks containing cast send commands and converts them to
AttackRunner-compatible JSONL (buy/sell/recenter/stake/unstake/mint_lp/burn_lp).
- Update scripts/harb-evaluator/red-team.sh: after each agent run, automatically
exports the attack sequence via export-attacks.py and replays it with
AttackRunner to capture structured snapshots in tmp/red-team-snapshots.jsonl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add an explanatory comment to uniswapV3SwapCallback clarifying that
address(this) is pre-funded by _replaySwap before pool.swap() is
called, so no inline mint is required (unlike uniswapV3MintCallback).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix fee attribution: distribute fees only to positions whose tick range
contains the active tick at close time (in-range weight), not by raw
liquidity. FLOOR is priced far below current tick and rarely earns fees;
the old approach would over-credit it and corrupt capital-efficiency and
net-P&L numbers. Fallback to raw-liquidity weighting with a WARN log
when no position is in range.
- Warn on first-close skip: when _closePosition finds no open record
(first recenter, before any tracking), log [TRACKER][WARN] instead of
silently returning so the gap is visible in reports.
- Add tick range assertion: require() that the incoming close snapshot
tick range matches the stored open record — a mismatch would mean IL
is computed across different ranges (apples vs oranges).
- Fix finalBlock accuracy: logSummary now calls
tracker.logFinalSummary(tracker.lastNotifiedBlock()) instead of
lastRecenterBlock, so the summary reflects the actual last replay block
rather than potentially hundreds of blocks early.
- Initialize lastRecenterBlock = block.number in StrategyExecutor
constructor to defer the first recenter attempt by recenterInterval
blocks and document the invariant.
- Extract shared FormatLib: _str(uint256) and _istr(int256) were
copy-pasted in both PositionTracker and StrategyExecutor. Extracted to
FormatLib.sol internal library; both contracts now use `using FormatLib`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PositionTracker.sol: tracks position lifecycle (open/close per
recenter), records tick ranges, liquidity, entry/exit blocks/timestamps,
token amounts (via LiquidityAmounts math), fees (proportional to
liquidity share), IL (LP exit value − HODL value at exit price), and
net P&L per position. Aggregates total fees, cumulative IL, net P&L,
rebalance count, Anchor time-in-range, and capital efficiency accumulators.
Logs with [TRACKER][TYPE] prefix; emits cumulative P&L every 500 blocks.
- Modify StrategyExecutor.sol: add IUniswapV3Pool + token0isWeth to
constructor (creates PositionTracker internally), call
tracker.notifyBlock() on every block for time-in-range, and call
tracker.recordRecenter() on each successful recenter. logSummary()
now delegates to tracker.logFinalSummary().
- Modify BacktestRunner.s.sol: pass sp.pool and token0isWeth to
StrategyExecutor constructor; log tracker address.
- forge fmt: reformat all backtesting scripts and affected src/test files
to project style (number_underscore=thousands, multiline_func_header=all).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add BacktestKraiken.sol: extends MockToken with Kraiken-compatible interface
(dual mint overloads — public mint(address,uint256) for EventReplayer and
restricted mint(uint256) for LiquidityManager; peripheryContracts() stubs
staking pool as address(0))
- Add KrAIkenDeployer.sol: library deploying OptimizerV3Push3 + LiquidityManager
on the shadow pool, wiring BacktestKraiken permissions, setting fee destination,
and funding LM with configurable initial mock-WETH capital (default 10 ETH)
- Add StrategyExecutor.sol: time-based recenter trigger (configurable block
interval, default 100 blocks); logs block, pre/post positions (Floor/Anchor/
Discovery tick ranges + liquidity), fees collected, and revert reason on skip;
negligible-impact assumption documented as TODO(#319)
- Modify EventReplayer.sol: add overloaded replay() accepting an optional
StrategyExecutor hook; maybeRecenter() called after each block advancement
without halting replay on failure
- Modify BacktestRunner.s.sol: replace tokenA/B with MockWETH + BacktestKraiken,
integrate KrAIkenDeployer + StrategyExecutor into broadcast block; configurable
via RECENTER_INTERVAL and INITIAL_CAPITAL_WETH env vars; executor.logSummary()
printed after replay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard final drift sample with `idx % LOG_INTERVAL != 0` to prevent
double-counting stats when totalReplayed is an exact multiple of
LOG_INTERVAL (the loop's _logCheckpoint already fired for that state)
- Hoist pool.slot0() before the guard and pass finalSqrtPrice/finalTick
to _logSummary(), eliminating the redundant slot0 read inside it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cache pool.tickSpacing() as immutable in EventReplayer constructor
to avoid a repeated external call per _replayMint() invocation
- Rename driftCount → driftCheckpoints for consistency with log label
- Add sqrtDriftBps to the per-checkpoint progress log line, using the
now-live lastExpectedSqrtPrice field (previously written but never read)
- Guard _replaySwap(): skip and count events where amountSpecified ≤ 0,
which would silently flip exact-input into exact-output mode
- Add a final drift sample after the while-loop for trailing events not
covered by the last LOG_INTERVAL checkpoint
- Move EventReplayer construction outside the broadcast block in
BacktestRunner (it uses vm.* cheat codes incompatible with real RPC)
- Change second vm.closeFile() from try/catch to a direct call so errors
surface rather than being silently swallowed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace hardcoded Infura API key with INFURA_API_KEY env var; fail fast
with a helpful message if unset and no --rpc-url is given
- Add onchain/script/backtesting/.gitignore (cache/) instead of relying on
the opaque root pattern; remove force-tracked cache/.gitkeep (mkdirSync
creates the directory at runtime)
- Document resume constraint: reliable only when both --start-block and
--end-block are explicit, or --output is set
- Fix batch-number display: derive batchNum inside the loop from the actual
`from` block so it stays correct when resumeFromBlock isn't BATCH_SIZE-aligned
- Guard log.logIndex === null consistently with blockNumber/transactionHash
- console.warn on decode errors instead of silently discarding them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add onchain/script/backtesting/fetch-events.ts — a tsx script that:
- Fetches Swap/Mint/Burn events from a Uniswap V3 pool via Infura (Base mainnet)
- Batches eth_getLogs in 2 000-block chunks with 100 ms inter-batch delay
- Decodes each log with viem and writes one JSON Line per event
- Supports resume: reads last block from existing cache file on re-run
- Retries with exponential back-off on 429 / rate-limit errors
- Prints per-batch progress: "Fetching blocks X-Y... N events (B/T batches)"
Also adds package.json, tsconfig.json, and cache/.gitkeep.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>