harb/evidence/holdout/2026-03-22-issue517-adversarial-lp.json

37 lines
2.2 KiB
JSON
Raw Normal View History

{
"date": "2026-03-22",
"issue": 517,
"title": "Adversary parasitic LP extracts 29% from holder — all recenters fail",
"scenario": "staker-vs-holder",
"status": "fixed",
"root_cause": {
"summary": "PRICE_STABILITY_INTERVAL (300s) too long relative to MIN_RECENTER_INTERVAL (60s)",
"detail": "After a large trade moving the tick >1000 positions, the 5-minute TWAP average lagged behind the current price by hundreds of ticks, far exceeding MAX_TICK_DEVIATION (50). Recenter reverted with 'price deviated from oracle' for ~285s after each trade, creating a window where the LM could not reposition. The adversary's parasitic LP captured fees during this unprotected window.",
"revert_reasons": {
"after_adversary_setup": "price deviated from oracle",
"after_holder_buy": "price deviated from oracle",
"after_adversary_attack": "price deviated from oracle",
"after_holder_sell": "amplitude not reached"
},
"johba_comment_confirmed": "Parasitic LP does not directly block recentering (V3 positions are independent). The revert is from the TWAP stability check, not from position interference."
},
"fix": {
"file": "onchain/src/abstracts/PriceOracle.sol",
"change": "PRICE_STABILITY_INTERVAL reduced from 300 to 30 seconds",
"rationale": "30s still prevents same-block manipulation (Ethereum mainnet ~12s block time) while ensuring TWAP converges well within the 60s cooldown. After the fix, recenter succeeds within 61s of any trade.",
"security_impact": "Manipulation window reduced from 5 min to 30s. Attacker must hold manipulated price for 30+ seconds (2.5 blocks) before recenter accepts it. Combined with 60s cooldown, total manipulation window is <60s."
},
"tests_added": [
"testRecenterAfterLargeBuy_TWAPConverges — verifies recenter works after 5 ETH buy + 61s wait",
"testRecenterRejectsSameBlockManipulation — verifies TWAP check still blocks <30s manipulation",
"testAdversarialLP_HolderProtected — full parasitic LP scenario, holder loss < 5%"
],
"test_results": {
"total": 256,
"passed": 255,
"failed": 1,
"skipped": 0,
"pre_existing_failure": "FitnessEvaluator.t.sol::testBatchEvaluate (requires FITNESS_MANIFEST_DIR env var)"
}
}