- Fix class-level NatSpec: use accurate wording (width computed from
anchorWidth param provided by Optimizer) instead of imprecise
LiquidityManager attribution
- Fix inline comment in _setAnchorPosition (same stale 1-100% claim)
- Update PRODUCT-TRUTH.md and ARCHITECTURE.md which had the same
incorrect 1-100% range claim
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the misleading "(1-100% width)" range claim from the ANCHOR NatSpec.
Anchor width enforcement lives in LiquidityManager, not this abstract, so
the comment is replaced with a note pointing to where enforcement actually occurs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Address review feedback:
- Add comment on FEE_DEST explaining why it differs from DeployBaseMainnet.sol:
on a Base mainnet fork, 0xf6a3...D9011 has contract code which triggers
feeDestinationLocked=true; keccak-derived address is a guaranteed EOA
- Expand bytecodes.txt EOF guard with a pipeline-mismatch warning log
- Fix STATE.md entry to use imperative verb form per project convention
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace Base Sepolia addresses with Base mainnet:
- V3_FACTORY: 0x33128a8fC17869897dcE68Ed026d694621f6FDfD
- SWAP_ROUTER: 0x2626664c2603336E57B271c5C0b26F421741e481
- NPM_ADDR: 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1
- Fix FEE_DEST to keccak-derived EOA (0x8A9145E1...9383) to avoid
feeDestination lock triggered by contract code at the old address
- Add vm.warp(block.timestamp + 600) alongside vm.roll in bootstrap
retry loop so _isPriceStable() TWAP check accumulates 300s+ history
- Guard vm.readLine(bytecodesFile) with empty-string break to prevent
vm.parseBytes("") revert on trailing EOF line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the MAX_ANCHOR_WIDTH=100 constant and the corresponding clamp on
anchorWidth in LiquidityManager.recenter(). The optimizer is now free to
choose any anchor width; evolution run 7 immediately exploited AW=153.
Update IOptimizer.sol NatSpec to reflect no clamping. Update the
testAnchorWidthAbove100IsClamped test to testAnchorWidthAbove100IsNotClamped,
asserting the tick range matches the full AW=150 width.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/mainnet-bootstrap.md: fix Step 4c to use SwapRouter02 7-field
struct (no deadline field); the 8-field ABI was for SwapRouter v1 but
the address is SwapRouter02
- docs/mainnet-bootstrap.md: correct Step 1 to no longer falsely claim
that pre-bootstrap transactions succeed when Forge aborts on simulation
failure; Step 1 now reflects the try/catch behaviour added below
- docs/mainnet-bootstrap.md: Step 6 drops --private-key flag (Foundry
ignores it when vm.startBroadcast(privateKey) is called internally)
and documents that the .secret seed-phrase file must be present
- docs/mainnet-bootstrap.md: remove no-op `export LM_ADDRESS="$LM_ADDRESS"`
- docs/mainnet-bootstrap.md: cite exact line range (101-145) in
Troubleshooting workaround instead of informal marker description
- onchain/script/DeployBase.sol: wrap liquidityManager.recenter() and
seed buy in try/catch so a fresh-pool TWAP revert skips the inline
bootstrap with a warning rather than aborting the entire simulation
- onchain/script/DeployBase.sol: fix --fork-url to --rpc-url in the
post-deploy console.log hint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add docs/mainnet-bootstrap.md with the full two-phase bootstrap
sequence: pool init, 300 s TWAP warm-up wait, first recenter + seed
buy (exact cast commands), 60 s cooldown wait, second recenter via
BootstrapVWAPPhase2.s.sol, and verification/troubleshooting steps.
Update the inline bootstrap comment in DeployBase.sol to warn that the
attempt always reverts on a fresh pool and direct operators to the new
runbook.
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>
Move the orphaned NatSpec block (originally for calculateParams) from
above getLiquidityParams to directly precede calculateParams, and give
getLiquidityParams only its own @inheritdoc block.
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>
vm.warp in forge script --broadcast only affects the local simulation
phase, not the actual Anvil node. The pool.observe([300,0]) call in
recenter() therefore reverted with OLD when Forge pre-flighted the
broadcast transactions on Anvil.
Fix:
- Remove the vm.warp + 2-recenter + SeedSwapper VWAP bootstrap from
DeployLocal.sol (only contract deployment now, simpler and reliable).
- Add bootstrap_vwap() to bootstrap-common.sh that uses Anvil RPC
evm_increaseTime + evm_mine to advance chain time before each recenter,
then executes a 0.5 ETH WETH->KRK seed swap between them.
- Call bootstrap_vwap() before fund_liquidity_manager() in both
containers/bootstrap.sh and ci-bootstrap.sh so the LM is seeded with
thin positions (1 ETH) during bootstrap, ensuring the 0.5 ETH swap
moves the price >400 ticks (amplitude gate).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forge resets block.timestamp to its pre-warp value after each state-changing
call (e.g. recenter()). The second vm.warp(block.timestamp + 301) in the VWAP
bootstrap was therefore warping to the same timestamp as the first warp, so
lastRecenterTime + 60 > block.timestamp and the second recenter() reverted
with "recenter cooldown".
Fix: store ts = block.timestamp + 301 before the first warp and increment it
explicitly (ts += 301) before the second warp, mirroring the same pattern
applied to VWAPFloorProtection.t.sol and SupplyCorruption.t.sol.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract magic number into named constant MAX_ANCHOR_WIDTH = 100 in LiquidityManager.sol
- Document effective ceiling in IOptimizer.sol natspec for anchorWidth return value
- Add testAnchorWidthAbove100IsClamped in LiquidityManager.t.sol asserting that
optimizer-returned anchorWidth=150 is silently clamped to 100 (not rejected)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace _positionEthValue() with _positionEthOnly() in FitnessEvaluator.t.sol.
The new function returns only the WETH component of each position (amount0 if
token0isWeth, else amount1), ignoring KRK token value entirely. This prevents
evolution from gaming the fitness metric by inflating KRK price through position
placement — the score now reflects actual ETH reserves only.
Also removes the now-unused FullMath import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add IOptimizer interface with getLiquidityParams() signature to IOptimizer.sol
so upgrade-compatibility is explicit and static analysis can catch ABI mismatches.
Update LiquidityManager to hold optimizer as IOptimizer instead of concrete Optimizer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#635
## Changes
The implementation is complete and committed. All 211 tests pass.
## Summary of changes
### `onchain/src/Optimizer.sol`
- **Replaced raw slot inputs** with normalized indicators in `getLiquidityParams()`:
- Slot 2 `pricePosition`: where current price sits within VWAP ± 11 000 ticks (0 = lower bound, 0.5e18 = at VWAP, 1e18 = upper bound)
- Slot 3 `volatility`: `|shortTwap − longTwap| / 1000 ticks`, capped at 1e18
- Slot 4 `momentum`: 0 = falling, 0.5e18 = flat, 1e18 = rising (5-min vs 30-min TWAP delta)
- Slot 5 `timeSinceRecenter`: `elapsed / 86400s`, capped at 1e18
- Slot 6 `utilizationRate`: 1e18 if current tick is within anchor position range, else 0
- **Extended `setDataSources()`** to accept `liquidityManager` + `token0isWeth` (needed for correct tick direction in momentum/utilizationRate)
- **Added `_vwapToTick()`** helper: converts `vwapX96 = price × 2⁹⁶` to tick via `sqrt(vwapX96) << 48`, with TickMath bounds clamping
- All slots gracefully default to 0 when data sources are unconfigured or TWAP history is insufficient (try/catch on `pool.observe()`)
### `onchain/src/OptimizerV3Push3.sol`
- Updated NatSpec to document the new `[0, 1e18]` slot semantics
### New tests (`onchain/test/`)
- `OptimizerNormalizedInputsTest`: 18 tests covering all new slots, token ordering, TWAP fallback, and a bounded fuzz test
- `mocks/MockPool.sol`: configurable `slot0()` + `observe()` with TWAP tick math
- `mocks/MockLiquidityManagerPositions.sol`: configurable anchor position bounds
Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/649
Reviewed-by: review_bot <review_bot@noreply.codeberg.org>
Three defensive layers so every Push3 program runs without reverting:
Layer A (transpiler/index.ts): assign bear defaults (CI=0, AS=0.3e18,
AW=100, DD=0.3e18) to all four outputs at the top of calculateParams.
Any output the evolved program does not overwrite keeps the safe default.
Layer B (transpiler/transpiler.ts): graceful stack underflow — dpop/bpop
return '0'/'false' instead of throwing, and the final output-pop falls
back to bear-default literals when fewer than 4 values remain on the
stack. Wrong output count no longer aborts transpilation.
Layer C (transpiler/transpiler.ts + index.ts): wrap the entire function
body in `unchecked {}` so integer overflow wraps (matching Push3), and
emit `(b == 0 ? 0 : a / b)` for every DYADIC./ (div-by-zero → 0,
matching Push3 no-op semantics).
Layer 2 (Optimizer.sol getLiquidityParams): clamp the three fraction
outputs (capitalInefficiency, anchorShare, discoveryDepth) to [0, 1e18]
after abi.decode so a buggy evolved program cannot produce out-of-range
values even if it runs without reverting.
Regenerated OptimizerV3Push3.sol with the updated transpiler; all 193
tests pass (34 Optimizer/OptimizerV3Push3 tests explicitly).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Optimizer.sol: move CALCULATE_PARAMS_GAS_LIMIT constant to top of
contract (after error declaration) to avoid mid-contract placement.
Expand natspec with EIP-150 63/64 note: callers need ~203 175 gas to
deliver the full 200 000 budget to the inner staticcall.
- Optimizer.sol: add ret.length < 128 guard before abi.decode in
getLiquidityParams(). Malformed return data (truncated / wrong ABI)
from an evolved program now falls back to _bearDefaults() instead of
propagating an unhandled revert. The 128-byte minimum is the ABI
encoding of (uint256, uint256, uint24, uint256) — four 32-byte slots.
- Optimizer.sol: add cross-reference comment to _bearDefaults() noting
that its values must stay in sync with LiquidityManager.recenter()'s
catch block to prevent silent divergence.
- FitnessEvaluator.t.sol: add CALCULATE_PARAMS_GAS_LIMIT mirror constant
(must match Optimizer.sol). Disqualify candidates whose measured gas
exceeds the production cap with fitness=0 and error="gas_over_limit"
— prevents the pipeline from selecting programs that are functionally
dead on-chain (would always produce bear defaults in production).
- batch-eval.sh: update output format comment to document the gas_used
field and over-gas-limit error object added by this feature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Optimizer.getLiquidityParams() now forwards calculateParams through a
staticcall capped at 200 000 gas. Programs that exceed the budget or
revert fall back to bear defaults (CI=0, AS=30%, AW=100, DD=0.3e18),
so a bloated evolved optimizer can never OOG-revert inside recenter().
- FitnessEvaluator.t.sol measures gas used by calculateParams against
fixed representative inputs (50% staked, 5% avg tax) after each
bootstrap. A soft penalty of GAS_PENALTY_FACTOR (1e13 wei/gas) is
subtracted from total fitness before the JSON score line is emitted.
Leaner programs win ties; gas_used is included in the output for
observability. At ~15k gas (current seed) the penalty is ~1.5e17 wei;
at the 200k hard cap boundary it reaches ~2e18 wei.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SPDX license:
- Restore GPL-3.0-or-later SPDX header to DeployBase.sol (removed by
the em-dash sed fix in an earlier commit).
SeedSwapper deduplication:
- Extract SeedSwapper into onchain/script/DeployCommon.sol — a single
canonical definition shared by both deploy scripts. This eliminates
duplicate Foundry artifacts (previously both DeployLocal.sol and
DeployBase.sol produced a SeedSwapper artifact, causing ambiguity for
verification and coverage tools).
- Remove inline SeedSwapper and redundant IWETH9 import from
DeployLocal.sol and DeployBase.sol; add `import "./DeployCommon.sol"`.
SeedSwapper hardening (in DeployCommon.sol):
- Replace magic-literal price sentinels with named constants
SQRT_PRICE_LIMIT_MIN / SQRT_PRICE_LIMIT_MAX.
- Wrap both weth.transfer() calls with require() so a non-standard
WETH9 false-return is caught rather than silently ignored.
- Add post-swap WETH sweep in executeSeedBuy(): if the price limit is
reached before the full input is spent, the residual WETH balance is
returned to `recipient` instead of being stranded in the contract.
bootstrap-common.sh:
- Normalise cumulativeVolume output through `cast to-dec` before the
string comparison, guarding against a future change in cast output
format (decimal vs hex).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 0.01 ETH seed swap only moved the tick 127 ticks from the start
and 37 ticks from the ANCHOR center — far below the 400-tick minimum
amplitude (2 × TICK_SPACING). As a result, the second recenter()
always reverted with "amplitude not reached", preventing VWAP bootstrap.
Root cause: SEED_SWAP_ETH was 1 % of SEED_LM_ETH. The ANCHOR
position holds ~25 % of SEED_LM_ETH as WETH across ~7 200 ticks, so
consuming half of that WETH (≈0.125 ETH) is already enough to move
the price 3 600 ticks past centre.
Fix: raise SEED_SWAP_ETH from 0.01 ether to 0.5 ether (50 % of
SEED_LM_ETH), giving a 4× margin over the minimum required. Verified
against a Base-Sepolia fork at block 20 000 000 (same environment as
CI): VWAP is now bootstrapped and cumulativeVolume > 0 after deployment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deploy scripts (DeployLocal.sol and DeployBase.sol) now execute a
seed buy + double-recenter sequence before handing control to users:
1. Temporarily grant deployer recenterAccess (via self as feeDestination)
2. Fund LM with a small amount and call recenter() -> places thin positions
3. SeedSwapper executes a small buy, generating a non-zero WETH fee
4. Second recenter() hits the cumulativeVolume==0 bootstrap path with
ethFee>0 -> _recordVolumeAndPrice fires -> cumulativeVolume>0
5. Revoke recenterAccess and restore the real feeDestination
After deployment, cumulativeVolume>0, so the bootstrap path is
unreachable by external users and cannot be front-run by an attacker
inflating the initial VWAP anchor with a whale buy.
Also adds:
- tools/deploy-optimizer.sh: verification step checks cumulativeVolume>0
after a fresh local deployment
- test_vwapBootstrappedBySeedTrade() in VWAPFloorProtection.t.sol:
confirms the deploy sequence (recenter + buy + recenter) leaves
cumulativeVolume>0 and getVWAP()>0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Skip UUPS upgradeTo: etch + vm.store ERC1967 implementation slot directly
(OptimizerV3Push3 is standalone, no UUPS inheritance needed for evolution)
- Use deployedBytecode (runtime) instead of bytecode (creation) for vm.etch
- Inject transpiled body into OptimizerV3.sol (has getLiquidityParams via Optimizer)
instead of using standalone OptimizerV3Push3.sol
- Wrap buy/sell/stake/unstake in try/catch — attack ops should not abort the batch
- Add /tmp read to fs_permissions for batch-eval manifest files
- Bootstrap recenter returns bool instead of reverting (soft-fail per candidate)
Remove unreachable else branch in VWAP recording logic. The branch was
only reachable if lastRecenterTick==0 and cumulativeVolume>0, which
requires tick==0 on the very first recenter — virtually impossible on a
live pool. Collapse else-if into else and delete the corresponding
testVWAPElseBranch test that exercised the path via vm.store.
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>
- LiquidityManager.setFeeDestination: add CREATE2 bypass guard — also
blocks re-assignment when the current feeDestination has since acquired
bytecode (was a plain address when set, contract deployed to it later)
- LiquidityManager.setFeeDestination: expand NatSpec to document the
EOA-mutability trade-off and the CREATE2 guard explicitly
- Test: add testSetFeeDestinationEOAToContract_Locks covering the
realistic EOA→contract transition (the primary lock-activation path)
- red-team.sh: add comment that DEPLOYER_PK is Anvil account-0 and must
only be used against a local ephemeral Anvil instance
- ARCHITECTURE.md: document feeDestination conditional-lock semantics and
contrast with Kraiken's strictly set-once liquidityManager/stakingPool
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `feeDestinationLocked` bool to LiquidityManager
- Replace one-shot setter with conditional trapdoor: EOAs may be set
repeatedly, but setting a contract address locks permanently
- Remove `AddressAlreadySet` error (superseded by the new lock mechanic)
- Replace fragile SLOT7 storage hack in red-team.sh with a proper
`setFeeDestination()` call using the deployer key
- Update tests: replace AddressAlreadySet test with three new tests
covering EOA multi-set, contract lock, and locked revert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add require(mantissa >= 0) guards in calculateParams before the uint256()
casts on inputs[0] and inputs[1], preventing negative int256 values from
wrapping to huge uint256 numbers and corrupting liquidity calculations.
Add two regression tests covering the revert paths for both slots.
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>