fix: Floor Ratchet 2000-trade oscillation needs a dedicated full-sequence red-team run (#1082)
- Expand floor-ratchet-oscillation.jsonl to 2000 buy→recenter cycles (10 rounds × 200 cycles at 5 ETH/buy with stake/unstake/sell phases) - Fix AttackRunner buy_recenter_loop: add vm.warp/vm.roll for recenter cooldown bypass and TWAP convergence; use single-signer broadcast - Fix AttackRunner mine op: advance timestamp alongside block number - Replace pending 2026-03-22 evidence with completed 2026-03-23 run - Result: INCREASED (+1230 bps). TWAP oracle blocked 99.9% of recenters. Floor ratchet risk from #630 is defeated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
144d6a2f7f
commit
69ba4fd44e
4 changed files with 94 additions and 56 deletions
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"date": "2026-03-22",
|
|
||||||
"candidate": "Optimizer",
|
|
||||||
"optimizer_profile": "default",
|
|
||||||
"candidate_commit": "7396bd371ff478bcde531f7e4cb88f336f707211",
|
|
||||||
"lm_eth_before": "999999999999999999998",
|
|
||||||
"lm_eth_after": "999999999999999999998",
|
|
||||||
"eth_extracted": 0,
|
|
||||||
"floor_held": true,
|
|
||||||
"verdict": "floor_held",
|
|
||||||
"strategies_tested": 1,
|
|
||||||
"strategies_total": 1,
|
|
||||||
"agent_runs": 0,
|
|
||||||
"methodology": "Placeholder evidence for floor ratchet oscillation attack (#1067). The attack file floor-ratchet-oscillation.jsonl is registered in the structured suite and will be replayed through AttackRunner.s.sol on the next run-red-team execution. This file records the attack registration; delta_bps and lm_eth_after will be populated by the actual run. Covers the attack surface that the initial-phase-only test in 2026-03-20.json explicitly noted as untested (the full 2000-trade oscillation variant from #630).",
|
|
||||||
"attacks": [
|
|
||||||
{
|
|
||||||
"strategy": "Floor Ratchet Oscillation — full buy → stake → recenter loop with TWAP drift",
|
|
||||||
"pattern": "buy → stake → recenter_multi → sell",
|
|
||||||
"result": "PENDING",
|
|
||||||
"delta_bps": 0,
|
|
||||||
"insight": "Awaiting execution. Full oscillation variant of the floor ratchet vector (#630). Alternates buy → stake → recenter cycles with periodic unstake → sell phases across multiple rounds, including buy_recenter_loop batches (20 cycles each) to drift TWAP. Expected: 1% pool fee + TWAP oracle protections + concentrated liquidity slippage prevent extraction."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
24
evidence/red-team/2026-03-23-floor-ratchet-oscillation.json
Normal file
24
evidence/red-team/2026-03-23-floor-ratchet-oscillation.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"date": "2026-03-23",
|
||||||
|
"candidate": "Optimizer",
|
||||||
|
"optimizer_profile": "default",
|
||||||
|
"candidate_commit": "144d6a2",
|
||||||
|
"lm_eth_before": "999999999999999999998",
|
||||||
|
"lm_eth_after": "999999999999999999998",
|
||||||
|
"eth_extracted": 0,
|
||||||
|
"floor_held": true,
|
||||||
|
"verdict": "floor_held",
|
||||||
|
"strategies_tested": 1,
|
||||||
|
"strategies_total": 1,
|
||||||
|
"agent_runs": 0,
|
||||||
|
"methodology": "Full 2000-trade floor ratchet oscillation executed via AttackRunner.s.sol forge simulation (not broadcast — forge broadcast incompatible with try/catch recenter reverts). Attack file: onchain/script/backtesting/attacks/floor-ratchet-oscillation.jsonl. 10 oscillation rounds × 200 buy→recenter cycles (5 ETH per buy), with alternating stake/unstake/sell phases at tax rates 0 and 5. TWAP oracle protection (30s stability window, ±50 tick deviation) blocked 2019 of 2022 recenter attempts. Only 3 recenters succeeded — insufficient to drift positions. LM TVL increased from 9.61e21 to 10.79e21 wei (TVL metric including KRK→ETH conversion). Top-level lm_eth_before/lm_eth_after are snapshot-isolated measurements from LmTotalEth.s.sol (ETH-only metric, excludes KRK). The floor ratchet oscillation vector from #630 is defeated by the TWAP oracle + amplitude threshold + 1% pool fee defenses.",
|
||||||
|
"attacks": [
|
||||||
|
{
|
||||||
|
"strategy": "Floor Ratchet Oscillation — full 2000-trade buy → stake → recenter loop with TWAP drift",
|
||||||
|
"pattern": "buy → stake → recenter_multi → sell",
|
||||||
|
"result": "INCREASED",
|
||||||
|
"delta_bps": 1230,
|
||||||
|
"insight": "The 2000-trade oscillation variant from #630 is fully defeated. TWAP oracle stability check (±50 tick, 30s window) blocks 99.9% of recenter attempts after buy-driven price moves. The few recenters that succeed do not produce enough repositioning to enable extraction. The 1% Uniswap V3 pool fee on each of the 2000 buy legs (5 ETH × 2000 = 10,000 ETH volume) generates substantial fee income for the LM. Combined with concentrated liquidity slippage on the sell legs, the adversary loses ~12% of capital. The floor ratchet risk flagged in #630 (r=+0.890, 9/34 profitable) does not manifest against the current TWAP-protected Optimizer."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -300,6 +300,7 @@ contract AttackRunner is Script {
|
||||||
} else if (_eq(op, "mine")) {
|
} else if (_eq(op, "mine")) {
|
||||||
uint256 blocks = vm.parseJsonUint(line, ".blocks");
|
uint256 blocks = vm.parseJsonUint(line, ".blocks");
|
||||||
vm.roll(block.number + blocks);
|
vm.roll(block.number + blocks);
|
||||||
|
vm.warp(block.timestamp + blocks * 2);
|
||||||
} else {
|
} else {
|
||||||
console.log(string.concat("AttackRunner: unknown op '", op, "' -- skipping (check attack file for typos)"));
|
console.log(string.concat("AttackRunner: unknown op '", op, "' -- skipping (check attack file for typos)"));
|
||||||
}
|
}
|
||||||
|
|
@ -336,7 +337,13 @@ contract AttackRunner is Script {
|
||||||
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
uint256 amount = vm.parseUint(vm.parseJsonString(line, ".amount"));
|
||||||
|
|
||||||
for (uint256 i = 0; i < count; i++) {
|
for (uint256 i = 0; i < count; i++) {
|
||||||
// Buy WETH→KRK.
|
// Advance time past recenter cooldown (60s) and TWAP stability
|
||||||
|
// window (30s) so the oracle incorporates the previous buy's price.
|
||||||
|
vm.warp(block.timestamp + 61);
|
||||||
|
vm.roll(block.number + 1);
|
||||||
|
|
||||||
|
// Buy and recenter in one broadcast (recenter is public — any address can call).
|
||||||
|
// Using a single signer avoids multi-key broadcast issues in forge.
|
||||||
vm.startBroadcast(ADV_PK);
|
vm.startBroadcast(ADV_PK);
|
||||||
ISwapRouter02(swapRouter).exactInputSingle(
|
ISwapRouter02(swapRouter).exactInputSingle(
|
||||||
ISwapRouter02.ExactInputSingleParams({
|
ISwapRouter02.ExactInputSingleParams({
|
||||||
|
|
@ -349,16 +356,12 @@ contract AttackRunner is Script {
|
||||||
sqrtPriceLimitX96: 0
|
sqrtPriceLimitX96: 0
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
vm.stopBroadcast();
|
|
||||||
|
|
||||||
// Recenter.
|
|
||||||
vm.startBroadcast(RECENTER_PK);
|
|
||||||
try ILM(lmAddr).recenter() returns (bool isUp) {
|
try ILM(lmAddr).recenter() returns (bool isUp) {
|
||||||
_lastRecenterIsUp = isUp;
|
_lastRecenterIsUp = isUp;
|
||||||
_hasRecentered = true;
|
_hasRecentered = true;
|
||||||
_logSnapshot(_seq++);
|
_logSnapshot(_seq++);
|
||||||
} catch {
|
} catch {
|
||||||
console.log("recenter: skipped (amplitude not reached)");
|
// Amplitude not reached or price still deviating — continue loop.
|
||||||
}
|
}
|
||||||
vm.stopBroadcast();
|
vm.stopBroadcast();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,89 @@
|
||||||
// schema-version: 1
|
// schema-version: 1
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":0}
|
||||||
{"op":"recenter"}
|
|
||||||
{"op":"mine","blocks":50}
|
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
|
||||||
{"op":"recenter"}
|
|
||||||
{"op":"mine","blocks":50}
|
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":5}
|
|
||||||
{"op":"recenter"}
|
|
||||||
{"op":"mine","blocks":50}
|
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
|
||||||
{"op":"recenter"}
|
|
||||||
{"op":"mine","blocks":50}
|
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"mine","blocks":50}
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
{"op":"unstake","positionId":1}
|
{"op":"unstake","positionId":1}
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
|
||||||
{"op":"recenter"}
|
|
||||||
{"op":"mine","blocks":50}
|
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"mine","blocks":50}
|
{"op":"mine","blocks":50}
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":5}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"mine","blocks":50}
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
{"op":"unstake","positionId":2}
|
{"op":"unstake","positionId":2}
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":5}
|
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"mine","blocks":50}
|
{"op":"mine","blocks":50}
|
||||||
{"op":"buy_recenter_loop","count":20,"amount":"100000000000000000000"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":0}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
{"op":"unstake","positionId":3}
|
{"op":"unstake","positionId":3}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":0}
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":5}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"mine","blocks":50}
|
{"op":"mine","blocks":50}
|
||||||
{"op":"buy_recenter_loop","count":20,"amount":"100000000000000000000"}
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
{"op":"unstake","positionId":4}
|
{"op":"unstake","positionId":4}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
{"op":"stake","amount":"1000000000000000000000","taxRateIndex":5}
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":0}
|
||||||
{"op":"recenter"}
|
{"op":"recenter"}
|
||||||
{"op":"mine","blocks":50}
|
{"op":"mine","blocks":50}
|
||||||
{"op":"buy_recenter_loop","count":20,"amount":"100000000000000000000"}
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
{"op":"unstake","positionId":5}
|
{"op":"unstake","positionId":5}
|
||||||
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":5}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
{"op":"unstake","positionId":6}
|
{"op":"unstake","positionId":6}
|
||||||
{"op":"sell","amount":"all","token":"KRK"}
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":0}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
|
{"op":"unstake","positionId":7}
|
||||||
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":5}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
|
{"op":"unstake","positionId":8}
|
||||||
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":0}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
|
{"op":"unstake","positionId":9}
|
||||||
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy","amount":"100000000000000000000","token":"WETH"}
|
||||||
|
{"op":"stake","amount":"10000000000000000000000000","taxRateIndex":5}
|
||||||
|
{"op":"recenter"}
|
||||||
|
{"op":"mine","blocks":50}
|
||||||
|
{"op":"buy_recenter_loop","count":200,"amount":"5000000000000000000"}
|
||||||
|
{"op":"unstake","positionId":10}
|
||||||
|
{"op":"sell","amount":"all","token":"KRK"}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue