Document MIN_RECENTER_INTERVAL (60 s, LiquidityManager.sol:61) and PRICE_STABILITY_INTERVAL (300 s, PriceOracle.sol:14) in docs/ARCHITECTURE.md and docs/PRODUCT-TRUTH.md so that agent-facing and product-facing copy stays traceable to source constants. Add an inline HTML comment in red-team-program.md next to the hardcoded 60s/300s sentence pointing to the two source constants, making drift detectable during code review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
Red-team mission: extract ETH from the KRAIKEN LiquidityManager
Goal
Extract ETH from the LiquidityManager. Make the LM end up with less total ETH than it started with.
Total LM ETH before: {{LM_ETH_BEFORE}} wei (free ETH + free WETH + ETH locked in all 3 Uni V3 positions)
The protocol's ETH is like gold in a cold-backed currency — it's "put to work" in Uniswap positions, but it should NEVER be extractable by an adversary. Your job is to prove otherwise.
The metric is simple: if LM total ETH goes down, you win.
Current Attack Target
| Field | Value |
|---|---|
| Candidate | {{CANDIDATE_NAME}} |
| Optimizer Profile | {{OPTIMIZER_PROFILE}} |
Use the optimizer profile to reason about this candidate's behavior:
- CI (concentration index %): higher → optimizer recenters more aggressively → more KRK minting opportunities
- AW (anchorWidth ticks): wider → liquidity spread over larger price range → less ETH per tick
- AS (anchorShare %): higher → more ETH locked in anchor position → different rebalancing behavior
- DD (discoveryDepth %): higher → more ETH in discovery position (above-price) → price-sensitive exposure
Contract addresses (local Anvil)
| Contract | Address |
|---|---|
| Kraiken (KRK) | {{KRK}} |
| Stake | {{STAKE}} |
| LiquidityManager | {{LM}} |
| OptimizerProxy | {{OPT}} |
| Pool (WETH/KRK 1%) | {{POOL}} |
| NonfungiblePosManager (NPM) | {{NPM}} |
| WETH | {{WETH}} |
| SwapRouter02 | {{SWAP_ROUTER}} |
RPC: http://localhost:8545 CAST binary: /home/debian/.foundry/bin/cast
Your accounts
Adversary — Anvil account 8 (your main account)
- Address: {{ADV_ADDR}}
- Private key: {{ADV_PK}}
- Balance: ~9000 ETH (10k minus 1000 ETH used to fund LM), 0 KRK
Recenter caller — Anvil account 2
- Address: {{RECENTER_ADDR}}
- Private key: {{RECENTER_PK}}
- Can call recenter() (public, TWAP-enforced)
Protocol mechanics
ethPerToken (the floor)
ethPerToken = (LM_native_ETH + LM_WETH) * 1e18 / adjusted_supply
adjusted_supply = KRK.outstandingSupply() - KRK_at_Stake
To DECREASE the floor you must either:
- Reduce LM's ETH/WETH holdings, OR
- Increase the adjusted outstanding supply of KRK
Three LM positions
The LiquidityManager maintains three Uniswap V3 positions:
- ANCHOR — straddles the current price; provides two-sided liquidity
- DISCOVERY — above current price; captures upside momentum
- FLOOR — a floor bid: ETH in, KRK out. Backing the floor price.
recenter()
Calling LiquidityManager.recenter() removes all three positions, mints or burns KRK
to rebalance, then re-deploys positions at the current price. It:
- Can mint NEW KRK (increasing supply → decreasing floor)
- Can burn KRK (decreasing supply → increasing floor)
- Moves ETH between positions Any account can call it (public). TWAP oracle enforces safety.
Staking
Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)
- taxRateIndex: 0–29 (index into the 30-element TAX_RATES array — not a raw percentage)
- KRK staked is held by the Stake contract (excluded from adjusted_supply)
- KRK in Stake does NOT count against the floor denominator
outstandingSupply() vs totalSupply()
KRK.outstandingSupply() = totalSupply() - balanceOf(liquidityManager)
LM-held KRK (in pool positions) is excluded from outstandingSupply.
The floor formula then additionally subtracts KRK at Stake to get adjusted_supply.
feeDestination is set to LM itself, so its KRK is already excluded by outstandingSupply().
Source Code (read-only reference)
Use the source code below to reason about internal state transitions, edge cases in tick math, exact mint/burn logic, optimizer parameter effects, and floor formula details. Do NOT attempt to deploy or modify contracts — these are for reference only.
LiquidityManager.sol
{{SOL_LM}}
ThreePositionStrategy.sol
{{SOL_THREE_POS}}
Optimizer.sol (base)
{{SOL_OPTIMIZER}}
OptimizerV3.sol (current candidate — reflects inject.sh output)
{{SOL_OPTIMIZERV3}}
VWAPTracker.sol
{{SOL_VWAP}}
PriceOracle.sol
{{SOL_PRICE_ORACLE}}
Kraiken.sol
{{SOL_KRAIKEN}}
Stake.sol
{{SOL_STAKE}}
Cast command patterns
Check total LM ETH (run after each strategy)
Measures free ETH + free WETH + ETH locked in all 3 Uni V3 positions.
CAST=/home/debian/.foundry/bin/cast
LM_ETH=$($CAST balance {{LM}} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
LM_WETH=$($CAST call {{WETH}} "balanceOf(address)(uint256)" {{LM}} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
SLOT0=$($CAST call {{POOL}} "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" --rpc-url http://localhost:8545)
CUR_TICK=$(echo "$SLOT0" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
TOKEN0_IS_WETH=$(python3 -c "print(1 if '{{WETH}}'.lower() < '{{KRK}}'.lower() else 0)")
POS_ETH=0
for STAGE in 0 1 2; do
POS=$($CAST call {{LM}} "positions(uint8)(uint128,int24,int24)" $STAGE --rpc-url http://localhost:8545)
LIQ=$(echo "$POS" | sed -n '1p' | sed 's/\[.*//;s/[[:space:]]//g')
TL=$(echo "$POS" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
TU=$(echo "$POS" | sed -n '3p' | sed 's/\[.*//;s/[[:space:]]//g')
POS_ETH=$(python3 -c "
import math
L,tl,tu,tc,t0w=int('$LIQ'),int('$TL'),int('$TU'),int('$CUR_TICK'),bool($TOKEN0_IS_WETH)
prev=int('$POS_ETH')
if L==0: print(prev); exit()
sa=math.sqrt(1.0001**tl); sb=math.sqrt(1.0001**tu); sc=math.sqrt(1.0001**tc)
if t0w:
e=L*(1/sa-1/sb) if tc<tl else (0 if tc>=tu else L*(1/sc-1/sb))
else:
e=L*(sb-sa) if tc>=tu else (0 if tc<tl else L*(sc-sa))
print(prev+int(e))
")
done
TOTAL=$(python3 -c "print(int('$LM_ETH')+int('$LM_WETH')+int('$POS_ETH'))")
echo "Total LM ETH: $TOTAL wei (free: $LM_ETH + $LM_WETH, positions: $POS_ETH)"
echo "Started with: {{LM_ETH_BEFORE}} wei"
python3 -c "b={{LM_ETH_BEFORE}}; a=int('$TOTAL'); d=b-a; print(f'Delta: {d} wei ({d*100//b if b else 0}% extracted)' if d>0 else f'Delta: {d} wei (LM gained ETH)')"
Wrap ETH to WETH
/home/debian/.foundry/bin/cast send {{WETH}} "deposit()" --value 100ether \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Approve token spend
/home/debian/.foundry/bin/cast send <TOKEN> "approve(address,uint256)" <SPENDER> \
115792089237316195423570985008687907853269984665640564039457584007913129639935 \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Buy KRK (WETH → KRK via SwapRouter)
# Must wrap ETH and approve WETH first
/home/debian/.foundry/bin/cast send {{SWAP_ROUTER}} \
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
"({{WETH}},{{KRK}},{{POOL_FEE}},{{ADV_ADDR}},<WETH_AMOUNT>,0,0)" \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Sell KRK (KRK → WETH via SwapRouter)
# Must approve KRK first
/home/debian/.foundry/bin/cast send {{SWAP_ROUTER}} \
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \
"({{KRK}},{{WETH}},{{POOL_FEE}},{{ADV_ADDR}},<KRK_AMOUNT>,0,0)" \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Stake KRK (snatch with no snatching)
# Approve KRK to Stake first
/home/debian/.foundry/bin/cast send {{STAKE}} \
"snatch(uint256,address,uint32,uint256[])" \
<KRK_AMOUNT> {{ADV_ADDR}} 0 "[]" \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Unstake KRK
/home/debian/.foundry/bin/cast send {{STAKE}} \
"exitPosition(uint256)" <POSITION_ID> \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Advance time (REQUIRED before each recenter call)
recenter() has a 60-second cooldown AND requires 300s of TWAP oracle history.
You MUST advance time before calling recenter:
/home/debian/.foundry/bin/cast rpc evm_increaseTime 600 --rpc-url http://localhost:8545
for i in $(seq 1 10); do /home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545; done
Trigger recenter (via account 2 — any address may call)
/home/debian/.foundry/bin/cast send {{LM}} "recenter()" \
--private-key {{RECENTER_PK}} --rpc-url http://localhost:8545
Read KRK balance
/home/debian/.foundry/bin/cast call {{KRK}} "balanceOf(address)(uint256)" {{ADV_ADDR}} \
--rpc-url http://localhost:8545
Read ETH balance
/home/debian/.foundry/bin/cast balance {{ADV_ADDR}} --rpc-url http://localhost:8545
Add LP position via NPM (mint)
# Must approve both tokens to NPM first. tickLower/tickUpper must be multiples of 200 (pool tickSpacing).
/home/debian/.foundry/bin/cast send {{NPM}} \
"mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))" \
"({{WETH}},{{KRK}},{{POOL_FEE}},<TICK_LOWER>,<TICK_UPPER>,<AMOUNT0>,<AMOUNT1>,0,0,{{ADV_ADDR}},<DEADLINE>)" \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Remove LP position via NPM (decreaseLiquidity then collect)
/home/debian/.foundry/bin/cast send {{NPM}} \
"decreaseLiquidity((uint256,uint128,uint256,uint256,uint256))" \
"(<TOKEN_ID>,<LIQUIDITY>,0,0,<DEADLINE>)" \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
/home/debian/.foundry/bin/cast send {{NPM}} \
"collect((uint256,address,uint128,uint128))" \
"(<TOKEN_ID>,{{ADV_ADDR}},340282366920938463463374607431768211455,340282366920938463463374607431768211455)" \
--private-key {{ADV_PK}} --rpc-url http://localhost:8545
Mine a block
/home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545
Snapshot and revert (for resetting between strategies)
# Take snapshot (returns ID — save it):
SNAP=$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://localhost:8545 | tr -d '"')
# Revert to snapshot (one-shot — take a new snapshot immediately after):
/home/debian/.foundry/bin/cast rpc anvil_revert $SNAP --rpc-url http://localhost:8545
Constraints
- feeDestination = LM itself — fees are NOT extracted, they accrue as LM liquidity. When computing ethPerToken, do NOT subtract KRK at feeDestination (it's the same as LM, and outstandingSupply() already excludes LM-held KRK).
- LM has ~1000 ETH reserve — proportional to your 10,000 ETH (10:1 ratio). This is a realistic attack scenario, not an empty vault.
- You MUST NOT call anvil_reset, anvil_setCode, or anvil_setStorageAt. These are infra cheats that invalidate the test. Use only swap/stake/LP/recenter protocol operations.
Rules
- You have ~9000 ETH (after funding LM with 1000 ETH). Start by wrapping some if you need WETH for swaps.
- Your goal is to make the LM's total ETH DECREASE vs the starting value ({{LM_ETH_BEFORE}} wei).
- Try at least 3 distinct strategies. After each attempt:
a. Run the total LM ETH check command above.
b. If total LM ETH DECREASED — report this as a SUCCESS and describe the exact steps.
c. If LM ETH held or INCREASED — revert to the snapshot and try a new strategy.
Remember:
anvil_revertis one-shot. Take a new snapshot immediately after reverting. - You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell).
- Be methodical. Report every strategy tried even if it failed.
- If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches.
- Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc.
- Start executing immediately. No lengthy planning — act, measure, iterate.
- For EVERY strategy attempted, record:
- Pattern: abstract op sequence (e.g., "buy → stake_all → recenter_multi → unstake → sell")
- Insight: WHY this worked or failed, referencing the optimizer profile ({{OPTIMIZER_PROFILE}}). For HELD/INCREASED: which mechanism defended the floor? How did CI/AW/AS/DD cause it? For DECREASED: which parameter combination created the vulnerability? Is it universal or optimizer-specific?
{{CROSS_CANDIDATE_SECTION}}
{{MEMORY_SECTION}}
Final report format
After trying all strategies, output a clearly structured report:
=== RED-TEAM REPORT ===
Candidate: {{CANDIDATE_NAME}}
Optimizer Profile: {{OPTIMIZER_PROFILE}}
lm_eth_before: <value> wei (total: free + positions)
STRATEGY 1: <name>
Pattern: <abstract op sequence e.g. "buy → recenter → sell">
Steps: <what you did>
lm_eth_after: <value> wei
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
Insight: <WHY this worked/failed given the optimizer profile>
STRATEGY 2: ...
...
=== CONCLUSION ===
ETH extracted: YES / NO
Winning strategy: <describe if YES, else "None">
Universal pattern: <would this likely work on other candidates? Why or why not?>
lm_eth_before: {{LM_ETH_BEFORE}} wei
lm_eth_after: <final value> wei