Commit graph

126 commits

Author SHA1 Message Date
openhands
ac4aa745f6 fix: fix: remove MAX_ANCHOR_WIDTH clamp in ThreePositionStrategy (#783)
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>
2026-03-14 23:21:30 +00:00
johba
6ff8282a7e Merge pull request 'fix: Remove recenterAccess — make recenter() public with TWAP enforcement (#706)' (#713) from fix/issue-706 into master 2026-03-14 10:48:59 +01:00
openhands
0d3aee15b4 fix: address AI review findings for #706 recenterAccess removal
- 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>
2026-03-14 09:15:48 +00:00
openhands
bc2ed50451 fix: V3_FACTORY address lacks a source comment (#730)
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>
2026-03-14 06:57:02 +00:00
openhands
cbab4c36da fix: NPM_ADDR may be Base Sepolia address in both files (#686)
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>
2026-03-14 02:22:51 +00:00
openhands
1a410a30b7 fix: Remove recenterAccess — make recenter() public with TWAP enforcement (#706) 2026-03-13 22:32:53 +00:00
openhands
39c25fa330 fix: LiquidityManager silently clamps anchorWidth to 100, undocumented upper bound (#689)
- 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>
2026-03-13 18:14:37 +00:00
openhands
defa1bfb6c fix: fix: Fitness metric should measure ETH only, not token value (#670)
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>
2026-03-13 09:11:31 +00:00
johba
8064623a54 fix: feat: Push3 input redesign — normalized indicators instead of raw protocol values (#635) (#649)
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>
2026-03-13 07:53:46 +01:00
johba
8e4bd905ac Merge pull request 'fix: fix: Bootstrap VWAP with seed trade during deployment (#567) (#567)' (#633) from fix/issue-567 into master 2026-03-13 03:17:30 +01:00
openhands
5d369cfab6 fix: address review findings for gas-limit fitness pressure (#637)
- 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>
2026-03-13 01:05:37 +00:00
openhands
c9c0ce5e95 fix: feat: Push3 evolution — gas limit as fitness pressure (#637)
- 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>
2026-03-13 00:25:49 +00:00
openhands
c05b20d640 fix: fix: Bootstrap VWAP with seed trade during deployment (#567) (#567)
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>
2026-03-12 21:15:35 +00:00
johba
c3c262a719 Merge pull request 'fix: revm evaluator — UUPS bypass, deployedBytecode, graceful attack ops' (#629) from fix/revm-evaluator into master
Reviewed-on: https://codeberg.org/johba/harb/pulls/629
Reviewed-by: review_bot <review_bot@noreply.codeberg.org>
2026-03-12 21:43:53 +01:00
openhands
87bb5859e2 fix: revm evaluator — UUPS bypass, deployedBytecode, graceful attack ops
- 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)
2026-03-12 19:54:58 +00:00
openhands
b8f5ed9411 fix: Dead code branch: lastRecenterTick==0 with cumulativeVolume>0 (#568)
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>
2026-03-12 19:21:11 +00:00
openhands
b902b89e3b fix: address review findings — CREATE2 guard, transition test, docs
- 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>
2026-03-12 17:13:50 +00:00
openhands
512640226b fix: fix: Conditional lock on feeDestination — lock when set to contract (#580) (#580)
- 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>
2026-03-12 16:13:44 +00:00
openhands
9bb223cf95 fix: Optimizer.sol also silently accepts negative mantissa inputs (#582)
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>
2026-03-12 15:41:39 +00:00
openhands
9c42947903 fix: address review findings in FitnessEvaluator (#604)
- Wrap upgradeTo() in try/catch: malformed candidate bytecode no longer
  aborts the entire batch; emit {"fitness":0,"error":"upgrade_failed"} and
  continue to the next candidate
- Bootstrap recenter: require() after 5 retry attempts so silent failure
  (all scores identically equal to free WETH only) is surfaced as a hard
  test failure rather than silently producing meaningless results
- mint_lp: capture the NPM tokenId returned by mint() and push it to
  _mintedNpmTokenIds; burn_lp now uses a 1-based index into that array
  (same pattern as stake/unstake), making attack files fork-block-independent
- Remove dead atkBaseSnap variable and its compiler-warning suppression
- Remove orphaned vm.snapshot() after vm.revertTo() in the attack loop
- Fix misleading comment on delete _stakedPositionIds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 12:31:11 +00:00
openhands
26b8876691 fix: feat: revm-based fitness evaluator for evolution at scale (#604)
Replace per-candidate Anvil+forge-script pipeline with in-process EVM
execution using Foundry's native revm backend, achieving 10-100× speedup
for evolutionary search at scale.

New files:
- onchain/test/FitnessEvaluator.t.sol — Forge test that forks Base once,
  deploys the full KRAIKEN stack, then for each candidate uses vm.etch to
  inject the compiled optimizer bytecode, UUPS-upgrades the proxy, runs all
  attack sequences with in-memory vm.snapshot/revertTo (no RPC overhead),
  and emits one {"candidate_id","fitness"} JSON line per candidate.
  Skips gracefully when BASE_RPC_URL is unset (CI-safe).

- tools/push3-evolution/revm-evaluator/batch-eval.sh — Wrapper that
  transpiles+compiles each candidate sequentially, writes a two-file
  manifest (ids.txt + bytecodes.txt), then invokes FitnessEvaluator.t.sol
  in a single forge test run and parses the score JSON from stdout.

Modified:
- tools/push3-evolution/evolve.sh — Adds EVAL_MODE env var (anvil|revm).
  When EVAL_MODE=revm, batch-scores every candidate in a generation with
  one batch-eval.sh call instead of N sequential fitness.sh processes;
  scores are looked up from the JSONL output in the per-candidate loop.
  Default remains EVAL_MODE=anvil for backward compatibility.

Key design decisions:
- Per-candidate Solidity compilation is unavoidable (each Push3 candidate
  produces different Solidity); the speedup is in the evaluation phase.
- vm.snapshot/revertTo in forge test are O(1) memory operations (true
  revm), not RPC calls — this is the core speedup vs Anvil.
- recenterAccess is set in bootstrap so TWAP stability checks are bypassed
  during attack sequences (mirrors the existing fitness.sh bootstrap).
- Test skips cleanly when BASE_RPC_URL is absent, keeping CI green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 11:54:41 +00:00
openhands
0dd764b8b3 fix: fix: Restore proper VWAP — gas-efficient volume-weighted pricing (revert TWAP) (#603)
- Replace pool.observe() TWAP price source with current pool tick (pool.slot0()) sampled once per recenter
- Remove _getTWAPOrFallback() and TWAPFallback event (added by PR #575)
- _scrapePositions now takes int24 currentTick instead of uint256 prevTimestamp; price computed via _priceAtTick before the burn loop
- Volume weighting (ethFee * 100) is unchanged — fees proxy swap volume over the recenter interval
- Direction fix from #566 (shouldRecordVWAP only on sell events) is preserved
- Remove test_twapReflectsAveragePriceNotJustLastSwap (tested reverted TWAP behaviour)
- ORACLE_CARDINALITY / increaseObservationCardinalityNext retained for _isPriceStable()
- All 188 tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 08:50:07 +00:00
openhands
f72f99aefa fix: Address review findings from PR #575
- Fix unsafe int32 intermediate cast: int56(int32(elapsed)) → int56(uint56(elapsed))
  to prevent TWAP tick sign inversion for intervals above int32 max (~68 years)
- Remove redundant lastRecenterTimestamp state variable; capture prevTimestamp
  from existing lastRecenterTime instead (saves ~20k gas per recenter)
- Use pool.increaseObservationCardinalityNext(ORACLE_CARDINALITY) in constructor
  instead of recomputing the pool address; extract magic 100 to named constant
- Add TWAPFallback(uint32 elapsed) event emitted when pool.observe() reverts
  so monitoring can distinguish degraded operation from normal bootstrap
- Remove conditional bypass paths in test_twapReflectsAveragePriceNotJustLastSwap;
  assert vwapAfter > 0 and vwapAfter > initialPriceX96 unconditionally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 10:02:47 +00:00
openhands
53c14745cb fix: Replace anchor-midpoint VWAP with pool.observe() TWAP oracle (#575)
- Add lastRecenterTimestamp to track recenter interval for TWAP
- Increase pool observation cardinality to 100 in constructor
- In _scrapePositions, use pool.observe([elapsed, 0]) to get TWAP tick
  over the full interval between recenters; falls back to anchor midpoint
  when elapsed==0 or pool.observe() reverts (insufficient history)
- Add test_twapReflectsAveragePriceNotJustLastSwap: verifies TWAP-based
  VWAP reflects the average price across the recenter interval, not just
  the last-swap anchor snapshot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 08:17:49 +00:00
johba
8c0683cbba Merge pull request 'fix: Red-team: replace ethPerToken with exact total-LM-ETH metric (#539)' (#540) from fix/issue-539 into master 2026-03-11 08:33:25 +01:00
openhands
6b77a493a2 fix: address review feedback — update stale comments and tighten test guard (#543)
- _scrapePositions natspec: 'ETH inflow' → 'ETH outflow / price fell or at bootstrap'
- Inline comment above VWAP block: remove inverted 'KRK sold out / ETH inflow' rationale,
  replace with a neutral forward-reference to recenter() where the direction logic lives
- VWAPFloorProtection.t.sol: remove unused Kraiken and forge-std/Test.sol imports
  (both are already provided by UniSwapHelper)
- test_floorConservativeAfterBuyOnlyAttack: add assertFalse(token0isWeth) guard so a
  future change to the setUp parameter cannot silently invert the gap-direction assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 04:11:02 +00:00
openhands
e35a805138 fix: Investigate: VWAP not preventing IL crystallization during buy-only recenter cycles (#543)
Investigation findings:
- VWAP WAS being fed during buy-only cycles (shouldRecordVWAP = true on ETH inflow / price rising).
  Over 80 buy-recenter cycles VWAP converged toward the inflated current price.
- When VWAP ≈ currentTick, mirrorTick = currentTick + vwapDistance ≈ currentTick, placing
  the floor near the inflated price.  Adversary sells back through the high floor, extracting
  nearly all LM ETH.
- Optimizer parameters (anchorShare, CI) were not the primary cause.

Fix (LiquidityManager.sol):
  Flip shouldRecordVWAP from buy direction to sell direction.  VWAP is now recorded only when
  price falls (ETH outflow / sell events) or at initial bootstrap (cumulativeVolume == 0).
  Buy-only attack cycles leave VWAP frozen at the historical baseline, keeping mirrorTick and
  the floor conservatively anchored far from the inflated current price.

Also updated onchain/AGENTS.md to document the corrected recording direction.

Regression test (VWAPFloorProtection.t.sol):
  - test_vwapNotInflatedByBuyOnlyAttack: asserts getVWAP() stays at bootstrap after N buy cycles.
  - test_floorConservativeAfterBuyOnlyAttack: asserts floor center is far below inflated tick.
  - test_vwapBootstrapsOnFirstFeeEvent: confirms bootstrap path unchanged.
  - test_recenterSucceedsOnSellDirectionWithoutReverts: confirms sell-direction recenters work.

All 187 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 03:31:45 +00:00
openhands
9832b454df fix: PR #551 review findings - OptimizerV3Push3.sol + Optimizer.sol
Critical bugs fixed:
- OptimizerV3Push3: Add input validation for mantissa (inputs[0].mantissa <= 1e18)
- OptimizerInput struct: Move to shared IOptimizer.sol to eliminate duplication
- Update imports in Optimizer.sol, OptimizerV3Push3.sol, and test file

Warnings addressed:
- Document unused variables (_d2-_d7) with comments in OptimizerV3Push3
- Add shift validation: require(inputs[k].shift == 0, "shift not yet supported")
- Fix recordRecenter error style: use UnauthorizedAccount custom error

Tests: All 32 Optimizer + OptimizerV3Push3 tests passing
2026-03-10 23:13:57 +00:00
openhands
2c21462e1e fix: LM: accrue fees as liquidity when feeDestination is self (#533)
When feeDestination == address(this), _scrapePositions() now skips the
fee safeTransfer calls so collected WETH/KRK stays in the LM balance
and is redeployed as liquidity on the next _setPositions() call.

Also fixes _getOutstandingSupply(): kraiken.outstandingSupply() already
subtracts balanceOf(liquidityManager), so when feeDestination IS the LM
the old code double-subtracted LM-held KRK, causing an arithmetic
underflow once positions were scraped.  The subtraction is now skipped
for the self-referencing case.

VWAP recording is refactored to a single unconditional block so it fires
regardless of fee destination.

New test testSelfFeeDestination_FeesAccrueAsLiquidity() demonstrates
that a two-recenter cycle with self-feeDestination completes without
underflow and without leaking WETH to any external address.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:11:41 +00:00
openhands
cfcf750084 fix: Backtesting #5: Position tracking + P&L metrics (#319)
- 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>
2026-02-27 11:23:18 +00:00
openhands
e925538309 fix: Remove dead Optimizer V2/V3 — Push3 is the active optimizer (#312)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 19:37:12 +00:00
openhands
e9370c143e fix: Test coverage: Kraiken + VWAPTracker to 100% (#283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 05:12:48 +00:00
openhands
93ddd28978 fix: Test coverage: Stake.sol to 100% (#284)
Add 11 new targeted tests in Stake.t.sol to cover all reachable
uncovered branches and the untested permitAndSnatch() function:

- testRevert_TaxRateOutOfBounds_InSnatch: taxRate >= TAX_RATES.length in snatch()
- testRevert_PositionNotFound_NonLastInLoop: PositionNotFound inside the multi-position loop
- testRevert_TaxTooLow_NonLastInLoop: TaxTooLow inside the multi-position loop
- testSnatch_ExitLastPosition: _exitPosition() path for last snatched position
- testRevert_ExceededAvailableStake: no available stake, no positions provided
- testRevert_TooMuchSnatch_AvailableExceedsNeed: post-exit excess stake check
- testRevert_PositionNotFound_InChangeTax: changeTax() on non-existent position
- testRevert_TaxTooLow_InChangeTax: changeTax() with same/lower tax rate
- testRevert_NoPermission_InExitPosition: exitPosition() by non-owner
- testRevert_PositionNotFound_InPayTax: payTax() on non-existent position
- testPermitAndSnatch: EIP-712 permit + snatch in one transaction

Coverage achieved:
  Lines:     99.33% (148/149)
  Statements: 99.40% (167/168)
  Branches:  93.55% (29/31)  — 2 unreachable dead-code branches remain
  Functions: 100.00% (15/15)

The 2 uncovered branches are dead code: the require() failure in
_shrinkPosition (caller always guards sharesToTake < pos.share) and
the PositionNotFound guard in exitPosition() (unreachable because
owner and creationTime are always set/cleared together, so
pos.owner==msg.sender implies pos.creationTime!=0 for any live caller).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 03:59:20 +00:00
openhands
9cce524b8a fix: Test coverage: LiquidityManager + Optimizer + OptimizerV3 to 100% (#285)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 02:41:02 +00:00
openhands
491c8f65b6 fix: resolve stack-too-deep in EthScarcityAbundance test
Extract _decodeVwapTick and _logEvent helpers to reduce stack depth
in _recenterAndLog. Also add via_ir=true to maxperf profile.
2026-02-23 17:10:01 +00:00
openhands
5e8a94b7a9 feat: Push3 → Solidity transpiler + OptimizerV3 port 2026-02-23 14:47:38 +00:00
johba
31063379a8 feat/ponder-lm-indexing (#142) 2026-02-18 00:19:05 +01:00
openhands
85350caf52 feat: OptimizerV3 with direct 2D staking-to-LP parameter mapping
Core protocol changes for launch readiness:

- OptimizerV3: binary bear/bull mapping from (staking%, avgTax) — avoids
  exploitable AW 30-90 kill zone. Bear: AS=30%, AW=100, CI=0, DD=0.3e18.
  Bull: AS=100%, AW=20, CI=0, DD=1e18. UUPS upgradeable with __gap[48].
- Directional VWAP: only records prices on ETH inflow (buys), preventing
  sell-side dilution of price memory
- Floor formula: unified max(scarcity, mirror, clamp) — VWAP mirror uses
  distance from adjusted VWAP as floor distance, no branching
- PriceOracle (M-1 fix): correct fallback TWAP divisor (60000s, not 300s)
- Access control (M-2 fix): deployer-only guard on one-time setters
- Recenter rate limit (M-3 fix): 60-second cooldown for open recenters
- Safe fallback params: recenter() optimizer-failure defaults changed from
  exploitable CI=50%/AW=50 to safe bear-mode CI=0/AW=100
- Recentered event for monitoring and indexing
- VERSION bump to 2, kraiken-lib COMPATIBLE_CONTRACT_VERSIONS updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:21:18 +00:00
openhands
21857ae8ca feat: protocol stats display + parameter sweep fuzzing infrastructure (#106)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:09:11 +00:00
openhands
d581b8394b fix(onchain): resolve KRK token supply corruption during recenter (#98)
PROBLEM:
Recenter operations were burning ~137,866 KRK tokens instead of minting
them, causing severe deflation when inflation should occur. This was due
to the liquidity manager burning ALL collected tokens from old positions
and then minting tokens for new positions separately, causing asymmetric
supply adjustments to the staking pool.

ROOT CAUSE:
During recenter():
1. _scrapePositions() collected tokens from old positions and immediately
   burned them ALL (+ proportional staking pool adjustment)
2. _setPositions() minted tokens for new positions (+ proportional
   staking pool adjustment)
3. The burn and mint operations used DIFFERENT totalSupply values in
   their proportion calculations, causing imbalanced adjustments
4. When old positions had more tokens than new positions needed, the net
   result was deflation

WHY THIS HAPPENED:
When KRK price increases (users buying), the same liquidity depth
requires fewer KRK tokens. The old code would:
- Burn 120k KRK from old positions (+ 30k from staking pool)
- Mint 10k KRK for new positions (+ 2.5k to staking pool)
- Net: -137.5k KRK total supply (WRONG!)

FIX:
1. Modified uniswapV3MintCallback() to use existing KRK balance first
   before minting new tokens
2. Removed burn() from _scrapePositions() - keep collected tokens
3. Removed burn() from end of recenter() - don't burn "excess"
4. Tokens held by LiquidityManager are already excluded from
   outstandingSupply(), so they don't affect staking calculations

RESULT:
Now during recenter, only the NET difference is minted or used:
- Collect old positions into LiquidityManager balance
- Use that balance for new positions
- Only mint additional tokens if more are needed
- Keep any unused balance for future recenters
- No more asymmetric burn/mint causing supply corruption

VERIFICATION:
- All 107 existing tests pass
- Added 2 new regression tests in test/SupplyCorruption.t.sol
- testRecenterDoesNotCorruptSupply: verifies single recenter preserves supply
- testMultipleRecentersPreserveSupply: verifies no accumulation over time

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 16:20:57 +00:00
johba
4277f19b68 feature/ci (#84)
Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/84
2026-02-02 19:24:57 +01:00
johba
d7c2184ccf Add Solidity linting with solhint, Foundry formatter, and pre-commit hooks (#51)
## Changes

### Configuration
- Added .solhint.json with recommended rules + custom config
  - 160 char line length (warn)
  - Double quotes enforcement (error)
  - Explicit visibility required (error)
  - Console statements allowed (scripts/tests need them)
  - Gas optimization warnings enabled
  - Ignores test/helpers/, lib/, out/, cache/, broadcast/

- Added foundry.toml [fmt] section
  - 160 char line length
  - 4-space tabs
  - Double quotes
  - Thousands separators for numbers
  - Sort imports enabled

- Added .lintstagedrc.json for pre-commit auto-fix
  - Runs solhint --fix on .sol files
  - Runs forge fmt on .sol files

- Added husky pre-commit hook via lint-staged

### NPM Scripts
- lint:sol - run solhint
- lint:sol:fix - auto-fix solhint issues
- format:sol - format with forge fmt
- format:sol:check - check formatting
- lint / lint:fix - combined commands

### Code Changes
- Added explicit visibility modifiers (internal) to constants in scripts and tests
- Fixed quote style in DeployLocal.sol
- All Solidity files formatted with forge fmt

## Verification
-  forge fmt --check passes
-  No solhint errors (warnings only)
-  forge build succeeds
-  forge test passes (107/107)

resolves #44

Co-authored-by: johba <johba@harb.eth>
Reviewed-on: https://codeberg.org/johba/harb/pulls/51
2025-10-04 15:17:09 +02:00
johba
af031877a5 fix position calculation 2025-09-23 11:46:57 +02:00
Your Name
0de1cffea8 another fixup of fuzzer 2025-09-16 22:46:43 +02:00
johba
c72fe56ad0 bounded buy/sell 2025-08-23 16:10:05 +02:00
johba
db23d756e9 fix: Optimize fuzzing and fix CSV buffer accumulation
- Deploy Uniswap factory once before all runs (saves ~1.16M gas per run)
- Fix CSV buffer accumulation bug by clearing buffer between runs
- Add clearCSV() function to CSVManager for proper buffer management
- Each fuzzing run now gets its own clean CSV with correct token0isWeth values
- Comment out failing console.log in Optimizer.t.sol to fix compilation

The token ordering now correctly alternates:
- Even seeds: token0isWeth = true (WETH < KRAIKEN)
- Odd seeds: token0isWeth = false (KRAIKEN < WETH)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 13:30:18 +02:00
johba
e5d47b1890 Merge branch 'anchor' 2025-08-19 11:49:50 +02:00
johba
f3047072f6 feat: Dynamic anchorWidth based on staking metrics
Replace hardcoded anchorWidth=100 with dynamic calculation that uses staking data as a decentralized oracle.

Changes:
- Add _calculateAnchorWidth() function to Optimizer.sol
- Base width 40% with adjustments based on staking percentage and average tax rate
- Staking adjustment: -20% to +20% (inverse relationship)
- Tax rate adjustment: -10% to +30% (direct relationship)
- Final range clamped to 10-80% for safety

Rationale:
- High staking % = bullish sentiment → narrower anchor (20-35%) for fee optimization
- Low staking % = bearish/uncertain → wider anchor (60-80%) for defensive positioning
- High tax rates = volatility expected → wider anchor to reduce rebalancing
- Low tax rates = stability expected → narrower anchor for fee collection

The Harberger tax mechanism acts as a prediction market where stakers' self-assessed valuations reveal market expectations.

Tests:
- Add comprehensive unit tests in test/Optimizer.t.sol
- Add mock contracts for testing (MockStake.sol, MockKraiken.sol)
- Manual verification confirms all scenarios calculate correctly

Documentation:
- Add detailed analysis of anchorWidth price ranges
- Add staking-based strategy recommendations
- Add verification of calculation logic

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 11:41:02 +02:00
johba
400ab325ed refactor: Complete project renaming from HARB/Harberger to KRAIKEN
- Updated all production code references from 'harb' to 'kraiken'
- Changed 'Harberger tax' references to 'self-assessed tax'
- Updated function names (_getHarbToken -> _getKraikenToken)
- Modified documentation and comments to reflect new branding
- Updated token symbol from HARB to KRAIKEN in tests
- Maintained backward compatibility with test variable names

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 11:05:08 +02:00
johba
2c69963151 feat: Add scenario recording and replay system for invariant debugging
Implements comprehensive fuzzing improvements to find and reproduce invariant violations:

Recording System:
- ScenarioRecorder captures exact trading sequences that violate invariants
- Exports to JSON, replay scripts, and human-readable summaries
- Unique Run IDs (format: YYMMDD-XXXX) for easy communication

Enhanced Fuzzing:
- ImprovedFuzzingAnalysis with larger trades (50-500 ETH) to reach discovery position
- Multiple strategies: Discovery Push, Whale Manipulation, Volatile Swings
- Successfully finds profitable scenarios with 66% success rate

Shell Scripts:
- run-recorded-fuzzing.sh: Automated fuzzing with recording and unique IDs
- replay-scenario.sh: One-command replay of specific scenarios

New Optimizers:
- ExtremeOptimizer: Tests extreme market conditions
- MaliciousOptimizer: Attempts to exploit the protocol

Documentation:
- Updated CLAUDE.md with complete recording workflow
- Enhanced 4-step debugging process
- Quick reference for team collaboration

This system successfully identifies and reproduces the discovery position exploit,
where traders can profit by pushing trades into the unused liquidity at extreme ticks.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-18 20:31:39 +02:00