- 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>
- M-2: update body to show current deployer-only setFeeDestination()
implementation and conditional locking; mark as partially resolved;
downgrade severity from Medium to Low; update conclusion entry
- I-1: mark as resolved — Recentered event declared at line 66 and
emitted at line 224 of LiquidityManager.sol
- I-2: correct VWAP direction (records on sells/ETH outflow, not buys);
update stale line reference from 146-158 to 177-191
- deployment.md §6.5: replace vague 'assess severity' step 1 with
concrete action (upgrade optimizer to bear defaults via §6.2)
- deployment.md §8 timeline: remove stale 'Set recenter access' row;
update 'First recenter' dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>