fix: Red-team: replace ethPerToken with exact total-LM-ETH metric (#539)

Replace the ethPerToken metric (free balance / adjusted supply) with total
LM ETH (free + WETH + position-locked) using a forge script with exact
Uni V3 integer math. Collapses 4+ RPC calls and Python float approximation
into a single forge script call using LiquidityAmounts + TickMath.

Also updates red-team prompt, report format, memory extraction, and adds
roadmap items for #536-#538 (backtesting pipeline, Push3 evolution).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-10 19:11:11 +00:00
parent 9832b454df
commit 0ddc1ccd80
3 changed files with 242 additions and 100 deletions

View file

@ -52,6 +52,9 @@ when the protocol changes — not the marketing copy.
- Staker governance for optimizer upgrades (vote with stake weight) - Staker governance for optimizer upgrades (vote with stake weight)
- On-chain training data → new optimizer contracts via Push3 transpiler - On-chain training data → new optimizer contracts via Push3 transpiler
- Remove admin key in favor of staker voting - Remove admin key in favor of staker voting
- Adversarial backtesting: replay red-team attack sequences against optimizer candidates (#536)
- Push3 optimizer evolution: mutate, score against attacks, select survivors (#537)
- Unified Push3 → deploy pipeline: transpile, compile, UUPS upgrade in one command (#538)
## Fee Destination ## Fee Destination

View file

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "forge-std/Script.sol";
import "uni-v3-lib/LiquidityAmounts.sol";
import "uni-v3-lib/TickMath.sol";
interface ILM {
function positions(uint8 stage) external view returns (uint128 liquidity, int24 tickLower, int24 tickUpper);
}
interface IWETH {
function balanceOf(address) external view returns (uint256);
}
/// @title LmTotalEth
/// @notice Read-only script: prints total ETH controlled by LiquidityManager
/// (free ETH + free WETH + ETH locked in all 3 Uni V3 positions).
/// @dev forge script script/LmTotalEth.s.sol --rpc-url $RPC_URL
/// Env: LM, WETH, POOL
contract LmTotalEth is Script {
function run() external view {
address lm = vm.envAddress("LM");
address weth = vm.envAddress("WETH");
address pool = vm.envAddress("POOL");
// Free balances
uint256 freeEth = lm.balance;
uint256 freeWeth = IWETH(weth).balanceOf(lm);
// Current sqrtPrice from pool
(uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0();
// Determine which token is WETH (token0 or token1)
bool wethIsToken0 = IUniswapV3Pool(pool).token0() == weth;
// Sum ETH in all 3 positions: FLOOR=0, ANCHOR=1, DISCOVERY=2
uint256 positionEth = 0;
for (uint8 stage = 0; stage < 3; stage++) {
(uint128 liquidity, int24 tickLower, int24 tickUpper) = ILM(lm).positions(stage);
if (liquidity == 0) continue;
uint160 sqrtA = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtB = TickMath.getSqrtRatioAtTick(tickUpper);
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtA, sqrtB, liquidity);
positionEth += wethIsToken0 ? amount0 : amount1;
}
uint256 total = freeEth + freeWeth + positionEth;
// Output as plain number for easy bash consumption
console2.log(total);
}
}

View file

@ -50,10 +50,37 @@ command -v claude &>/dev/null || die "claude CLI not found (install: npm i -g
command -v python3 &>/dev/null || die "python3 not found" command -v python3 &>/dev/null || die "python3 not found"
command -v jq &>/dev/null || die "jq not found" command -v jq &>/dev/null || die "jq not found"
# ── 1. Verify stack is running ───────────────────────────────────────────────── # ── 1. Fresh stack — tear down, rebuild, wait for bootstrap ────────────────────
log "Verifying Anvil is accessible at $RPC_URL ..." log "Rebuilding fresh stack ..."
cd "$REPO_ROOT"
# Free RAM: drop caches
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' 2>/dev/null || true
# Tear down completely (volumes too — clean anvil state)
sudo docker compose down -v >/dev/null 2>&1 || true
sleep 3
# Bring up
sudo docker compose up -d >/dev/null 2>&1 \
|| die "docker compose up -d failed"
# Wait for bootstrap to complete (max 120s)
log "Waiting for bootstrap ..."
for i in $(seq 1 40); do
if sudo docker logs harb-bootstrap-1 2>&1 | grep -q "Bootstrap complete"; then
log " Bootstrap complete (${i}x3s)"
break
fi
if [[ $i -eq 40 ]]; then
die "Bootstrap did not complete within 120s"
fi
sleep 3
done
# Verify Anvil responds
"$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \ "$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| die "Anvil not accessible at $RPC_URL — run: ./scripts/dev.sh start" || die "Anvil not accessible at $RPC_URL after stack start"
# ── 2. Read contract addresses ───────────────────────────────────────────────── # ── 2. Read contract addresses ─────────────────────────────────────────────────
[[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)" [[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)"
@ -85,23 +112,64 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \
"$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL") "$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL")
log " Pool: $POOL" log " Pool: $POOL"
# ── 3. Grant recenterAccess to account 2 ────────────────────────────────────── # ── 3a. Grant recenterAccess FIRST (while original feeDestination is still set) ──
# Done BEFORE the snapshot so every revert restores account 2's access.
# LM.recenterAccess is a single address slot — replace it with account 2.
# Only the feeDestination is authorised to call setRecenterAccess().
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) ..."
FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \ FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \
|| die "Failed to read feeDestination() from LM" || die "Failed to read feeDestination() from LM"
FEE_DEST=$(echo "$FEE_DEST" | tr -d '[:space:]') FEE_DEST=$(echo "$FEE_DEST" | tr -d '[:space:]')
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) via feeDestination ($FEE_DEST) ..."
"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \ "$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \
|| die "anvil_impersonateAccount $FEE_DEST failed" || die "anvil_impersonateAccount $FEE_DEST failed"
"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \ "$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \
"$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \ "$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \
|| die "setRecenterAccess($RECENTER_ADDR) failed — check that feeDestination is correct" || die "setRecenterAccess($RECENTER_ADDR) failed"
"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \ "$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \
|| die "anvil_stopImpersonatingAccount $FEE_DEST failed" || die "anvil_stopImpersonatingAccount $FEE_DEST failed"
log " recenterAccess granted" log " recenterAccess granted"
# ── 3b. Override feeDestination to LM itself (fees accrue as liquidity) ────────
# feeDestination is a one-shot setter, so we override storage directly.
# Slot 7 contains feeDestination (packed with other data in upper bytes).
log "Setting feeDestination to LM ($LM) ..."
SLOT7=$("$CAST" storage "$LM" 7 --rpc-url "$RPC_URL" | tr -d '[:space:]')
UPPER=${SLOT7:0:26}
LM_LOWER=$(echo "$LM" | tr '[:upper:]' '[:lower:]' | sed 's/0x//')
NEW_SLOT7="${UPPER}${LM_LOWER}"
"$CAST" rpc --rpc-url "$RPC_URL" anvil_setStorageAt "$LM" "0x7" "$NEW_SLOT7" \
|| die "anvil_setStorageAt for feeDestination failed"
VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | tr -d '[:space:]')
log " feeDestination set to: $VERIFY"
[[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY"
# ── 3c. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
# Send ETH as WETH (LM uses WETH internally), then recenter to deploy into positions.
# Without recenter, the ETH sits idle and the first recenter mints massive KRK.
log "Funding LM with 1000 ETH ..."
# Wrap to WETH and transfer to LM
"$CAST" send "$WETH" "deposit()" --value 1000ether \
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| die "Failed to wrap ETH"
"$CAST" send "$WETH" "transfer(address,uint256)" "$LM" 1000000000000000000000 \
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| die "Failed to transfer WETH to LM"
# Recenter to deploy the new WETH into positions (establishes realistic baseline)
log "Recentering to deploy funded WETH into positions ..."
"$CAST" send "$LM" "recenter()" \
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|| log " WARNING: initial recenter failed (may need amplitude — mining blocks)"
# Mine blocks and retry if needed
for _i in $(seq 1 3); do
for _b in $(seq 1 50); do
"$CAST" rpc evm_mine --rpc-url "$RPC_URL" >/dev/null 2>&1
done
"$CAST" send "$LM" "recenter()" \
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 && break
done
LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
LM_WETH=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
log " LM after recenter: ETH=$LM_ETH WETH=$LM_WETH"
# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ───── # ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ─────
log "Taking Anvil snapshot..." log "Taking Anvil snapshot..."
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"') SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
@ -117,43 +185,18 @@ cleanup() {
} }
trap cleanup EXIT INT TERM trap cleanup EXIT INT TERM
# ── Helper: compute ethPerToken (mirrors floor.ts getEthPerToken) ────────────── # ── Helper: compute total ETH controlled by LM ────────────────────────────────
# ethPerToken = (lm_native_eth + lm_weth) * 1e18 / adjusted_outstanding_supply # Total = free ETH + free WETH + ETH locked in all 3 Uni V3 positions
# adjusted_supply = outstandingSupply() - KRK_at_feeDestination - KRK_at_stakingPool # This is the real metric: "can the adversary extract ETH from the protocol?"
compute_eth_per_token() { # Uses a forge script with exact Uni V3 integer math (LiquidityAmounts + TickMath)
local lm_eth lm_weth supply fee_bal stake_bal adj_supply # instead of multiple cast calls + Python float approximation.
compute_lm_total_eth() {
lm_eth=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') local output
lm_weth=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" \ output=$(LM="$LM" WETH="$WETH" POOL="$POOL" \
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') /home/debian/.foundry/bin/forge script script/LmTotalEth.s.sol \
supply=$("$CAST" call "$KRK" "outstandingSupply()(uint256)" \ --rpc-url "$RPC_URL" --root "$REPO_ROOT/onchain" 2>&1)
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') # forge script prints "== Logs ==" then " <value>" — extract the number
echo "$output" | awk '/^== Logs ==/{getline; gsub(/^[[:space:]]+/,""); print; exit}'
# Fee destination: read from contract (set at deploy time, may differ per fork)
local fee_dest
fee_dest=$("$CAST" call "$LM" "feeDestination()(address)" \
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
fee_bal=0
local zero="0x0000000000000000000000000000000000000000"
if [[ "${fee_dest,,}" != "${zero,,}" ]]; then
fee_bal=$("$CAST" call "$KRK" "balanceOf(address)(uint256)" "$fee_dest" \
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || echo 0)
fi
# Staking pool: use the deployed Stake address (mirrors peripheryContracts()[1])
stake_bal=$("$CAST" call "$KRK" "balanceOf(address)(uint256)" "$STAKE" \
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || echo 0)
python3 - <<PYEOF
e = int('${lm_eth:-0}' or 0)
w = int('${lm_weth:-0}' or 0)
s = int('${supply:-0}' or 0)
f = int('${fee_bal:-0}' or 0)
k = int('${stake_bal:-0}' or 0)
adj = s - f - k
print(0 if adj <= 0 else (e + w) * 10**18 // adj)
PYEOF
} }
# ── Helper: extract strategy findings from stream-json and append to memory ──── # ── Helper: extract strategy findings from stream-json and append to memory ────
@ -169,7 +212,7 @@ extract_memory() {
run_num=1 run_num=1
fi fi
python3 - "$stream_file" "$memory_file" "$run_num" "$FLOOR_BEFORE" <<'PYEOF' python3 - "$stream_file" "$memory_file" "$run_num" "$LM_ETH_BEFORE" <<'PYEOF'
import json, sys, re import json, sys, re
from datetime import datetime, timezone from datetime import datetime, timezone
@ -177,9 +220,9 @@ stream_file = sys.argv[1]
memory_file = sys.argv[2] memory_file = sys.argv[2]
run_num = int(sys.argv[3]) run_num = int(sys.argv[3])
try: try:
floor_before = int(sys.argv[4]) lm_eth_before = int(sys.argv[4])
except (ValueError, IndexError): except (ValueError, IndexError):
print(" extract_memory: invalid floor_before value, skipping", file=sys.stderr) print(" extract_memory: invalid lm_eth_before value, skipping", file=sys.stderr)
sys.exit(0) sys.exit(0)
texts = [] texts = []
@ -209,7 +252,7 @@ for text in texts:
current = { current = {
"strategy": strat_match.group(1).strip(), "strategy": strat_match.group(1).strip(),
"steps": "", "steps": "",
"floor_after": None, "lm_eth_after": None,
"insight": "" "insight": ""
} }
@ -217,7 +260,7 @@ for text in texts:
# Capture floor readings — take the last match in the block (most recent value) # Capture floor readings — take the last match in the block (most recent value)
floor_matches = list(re.finditer(r"(?:floor|ethPerToken)[^\d]*?(\d{4,})\s*(?:wei)?", text, re.IGNORECASE)) floor_matches = list(re.finditer(r"(?:floor|ethPerToken)[^\d]*?(\d{4,})\s*(?:wei)?", text, re.IGNORECASE))
if floor_matches: if floor_matches:
current["floor_after"] = int(floor_matches[-1].group(1)) current["lm_eth_after"] = int(floor_matches[-1].group(1))
# Capture insights # Capture insights
for pattern in [r"[Kk]ey [Ii]nsight:\s*(.+)", r"[Ii]nsight:\s*(.+)", r"(?:discovered|learned|realized)\s+(?:that\s+)?(.+)"]: for pattern in [r"[Kk]ey [Ii]nsight:\s*(.+)", r"[Ii]nsight:\s*(.+)", r"(?:discovered|learned|realized)\s+(?:that\s+)?(.+)"]:
@ -237,11 +280,11 @@ if current:
ts = datetime.now(timezone.utc).isoformat() ts = datetime.now(timezone.utc).isoformat()
with open(memory_file, "a") as f: with open(memory_file, "a") as f:
for s in strategies: for s in strategies:
fa = s["floor_after"] if s.get("floor_after") is not None else floor_before fa = s["lm_eth_after"] if s.get("lm_eth_after") is not None else lm_eth_before
delta_bps = round((fa - floor_before) * 10000 / floor_before) if floor_before else 0 delta_bps = round((fa - lm_eth_before) * 10000 / lm_eth_before) if lm_eth_before else 0
if fa < floor_before: if fa < lm_eth_before:
result = "DECREASED" result = "DECREASED"
elif fa > floor_before: elif fa > lm_eth_before:
result = "INCREASED" result = "INCREASED"
else: else:
result = "HELD" result = "HELD"
@ -251,8 +294,8 @@ with open(memory_file, "a") as f:
"ts": ts, "ts": ts,
"strategy": s["strategy"][:100], "strategy": s["strategy"][:100],
"steps": s["steps"][:300].rstrip("; "), "steps": s["steps"][:300].rstrip("; "),
"floor_before": floor_before, "lm_eth_before": lm_eth_before,
"floor_after": fa, "lm_eth_after": fa,
"delta_bps": delta_bps, "delta_bps": delta_bps,
"result": result, "result": result,
"insight": s["insight"][:300] "insight": s["insight"][:300]
@ -284,10 +327,10 @@ if len(all_entries) > 50:
PYEOF PYEOF
} }
# ── 5. Read floor_before ─────────────────────────────────────────────────────── # ── 5. Read lm_eth_before ───────────────────────────────────────────────────────
log "Reading floor before agent run..." log "Reading floor before agent run..."
FLOOR_BEFORE=$(compute_eth_per_token) LM_ETH_BEFORE=$(compute_lm_total_eth)
log " floor_before = $FLOOR_BEFORE wei/token" log " lm_eth_before = $LM_ETH_BEFORE wei"
# ── 6. Build agent prompt ────────────────────────────────────────────────────── # ── 6. Build agent prompt ──────────────────────────────────────────────────────
@ -323,15 +366,18 @@ PYEOF
fi fi
PROMPT=$(cat <<PROMPT_EOF PROMPT=$(cat <<PROMPT_EOF
# Red-team mission: break the KRAIKEN protocol floor # Red-team mission: extract ETH from the KRAIKEN LiquidityManager
## Goal ## Goal
Make ethPerToken **decrease** compared to its current value. **Extract ETH from the LiquidityManager.** Make the LM end up with less total ETH than it started with.
Current ethPerToken: ${FLOOR_BEFORE} wei/token Total LM ETH before: ${LM_ETH_BEFORE} wei
(free ETH + free WETH + ETH locked in all 3 Uni V3 positions)
ethPerToken measures: (LiquidityManager native ETH + WETH) × 1e18 ÷ adjusted_outstanding_supply The protocol's ETH is like gold in a cold-backed currency — it's "put to work" in Uniswap positions,
It is the floor price — the minimum ETH per KRK token the protocol will pay. 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.
--- ---
@ -407,15 +453,38 @@ The floor formula then additionally subtracts KRK at Stake and feeDestination to
## Cast command patterns ## Cast command patterns
### Check the floor (run after each strategy) ### Check total LM ETH (run after each strategy)
Measures free ETH + free WETH + ETH locked in all 3 Uni V3 positions.
\`\`\`bash \`\`\`bash
LM_ETH=\$(/home/debian/.foundry/bin/cast balance ${LM} --rpc-url http://localhost:8545) CAST=/home/debian/.foundry/bin/cast
LM_WETH=\$(/home/debian/.foundry/bin/cast call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545) LM_ETH=\$(\$CAST balance ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
SUPPLY=\$(/home/debian/.foundry/bin/cast call ${KRK} "outstandingSupply()(uint256)" --rpc-url http://localhost:8545) LM_WETH=\$(\$CAST call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
FEE_DEST=\$(/home/debian/.foundry/bin/cast call ${LM} "feeDestination()(address)" --rpc-url http://localhost:8545) SLOT0=\$(\$CAST call ${POOL} "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" --rpc-url http://localhost:8545)
FEE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" \$FEE_DEST --rpc-url http://localhost:8545) CUR_TICK=\$(echo "\$SLOT0" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
STAKE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${STAKE} --rpc-url http://localhost:8545) TOKEN0_IS_WETH=\$(python3 -c "print(1 if '${WETH}'.lower() < '${KRK}'.lower() else 0)")
python3 -c "e=\$LM_ETH; w=\$LM_WETH; s=\$SUPPLY; f=\$FEE_BAL; k=\$STAKE_BAL; adj=s-f-k; print('ethPerToken:', (e+w)*10**18//adj if adj>0 else 0, 'wei/token')" 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 ### Wrap ETH to WETH
@ -519,19 +588,30 @@ SNAP=\$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://local
--- ---
## 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 ## Rules
1. You have 10 000 ETH. Start by wrapping some if you need WETH for swaps. 1. You have 10 000 ETH. Start by wrapping some if you need WETH for swaps.
2. Your goal is to make ethPerToken DECREASE vs the starting value (${FLOOR_BEFORE} wei/token). 2. Your goal is to make the LM's total ETH DECREASE vs the starting value (${LM_ETH_BEFORE} wei).
3. Try at least 3 distinct strategies. After each attempt: 3. Try at least 3 distinct strategies. After each attempt:
a. Run the floor-check command above to measure ethPerToken. a. Run the total LM ETH check command above.
b. If the floor DECREASED — report this as a SUCCESS and describe the exact steps. b. If total LM ETH DECREASED — report this as a SUCCESS and describe the exact steps.
c. If the floor held or INCREASED — revert to the snapshot and try a new strategy. c. If LM ETH held or INCREASED — revert to the snapshot and try a new strategy.
Remember: \`anvil_revert\` is one-shot. Take a new snapshot immediately after reverting. Remember: \`anvil_revert\` is one-shot. Take a new snapshot immediately after reverting.
4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell). 4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell).
5. Be methodical. Report every strategy tried even if it failed. 5. Be methodical. Report every strategy tried even if it failed.
6. If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches. 6. If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches.
7. Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc. 7. Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc.
8. Start executing immediately. No lengthy planning — act, measure, iterate.
--- ---
@ -544,21 +624,21 @@ After trying all strategies, output a clearly structured report:
\`\`\` \`\`\`
=== RED-TEAM REPORT === === RED-TEAM REPORT ===
floor_before: <value> wei/token lm_eth_before: <value> wei (total: free + positions)
STRATEGY 1: <name> STRATEGY 1: <name>
Steps: <what you did> Steps: <what you did>
floor_after: <value> lm_eth_after: <value> wei
Result: INCREASED / HELD / DECREASED Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
STRATEGY 2: ... STRATEGY 2: ...
... ...
=== CONCLUSION === === CONCLUSION ===
Floor broken: YES / NO ETH extracted: YES / NO
Winning strategy: <describe if YES, else "None"> Winning strategy: <describe if YES, else "None">
floor_before: ${FLOOR_BEFORE} wei/token lm_eth_before: ${LM_ETH_BEFORE} wei
floor_after: <final value> lm_eth_after: <final value> wei
\`\`\` \`\`\`
PROMPT_EOF PROMPT_EOF
) )
@ -601,37 +681,37 @@ with open(sys.argv[1]) as f:
PYEOF PYEOF
# If the agent crashed and produced no readable output, treat as an infra error # If the agent crashed and produced no readable output, treat as an infra error
# rather than silently reporting FLOOR HELD (a false pass). # rather than silently reporting ETH SAFE (a false pass).
if [[ $AGENT_EXIT -ne 0 && ! -s "$REPORT" ]]; then if [[ $AGENT_EXIT -ne 0 && ! -s "$REPORT" ]]; then
die "claude agent failed (exit $AGENT_EXIT) with no readable output — see $STREAM_LOG" die "claude agent failed (exit $AGENT_EXIT) with no readable output — see $STREAM_LOG"
fi fi
# ── 8. Read floor_after ──────────────────────────────────────────────────────── # ── 8. Read lm_eth_after ────────────────────────────────────────────────────────
log "Reading floor after agent run..." log "Reading floor after agent run..."
FLOOR_AFTER=$(compute_eth_per_token) LM_ETH_AFTER=$(compute_lm_total_eth)
# ── 8a. Extract and persist strategy findings ────────────────────────────────── # ── 8a. Extract and persist strategy findings ──────────────────────────────────
log "Extracting strategy findings from agent output..." log "Extracting strategy findings from agent output..."
extract_memory "$STREAM_LOG" extract_memory "$STREAM_LOG"
log " floor_after = $FLOOR_AFTER wei/token" log " lm_eth_after = $LM_ETH_AFTER wei"
# ── 9. Summarise results ─────────────────────────────────────────────────────── # ── 9. Summarise results ───────────────────────────────────────────────────────
log "" log ""
log "=== RED-TEAM SUMMARY ===" log "=== RED-TEAM SUMMARY ==="
log "" log ""
log " floor_before : $FLOOR_BEFORE wei/token" log " lm_eth_before : $LM_ETH_BEFORE wei"
log " floor_after : $FLOOR_AFTER wei/token" log " lm_eth_after : $LM_ETH_AFTER wei"
log "" log ""
BROKE=false BROKE=false
if python3 -c "import sys; sys.exit(0 if int('$FLOOR_AFTER') < int('$FLOOR_BEFORE') else 1)"; then if python3 -c "import sys; sys.exit(0 if int('$LM_ETH_AFTER') < int('$LM_ETH_BEFORE') else 1)"; then
BROKE=true BROKE=true
fi fi
if [[ "$BROKE" == "true" ]]; then if [[ "$BROKE" == "true" ]]; then
DELTA=$(python3 -c "print($FLOOR_BEFORE - $FLOOR_AFTER)") DELTA=$(python3 -c "print($LM_ETH_BEFORE - $LM_ETH_AFTER)")
log " RESULT: FLOOR BROKEN ❌" log " RESULT: ETH EXTRACTED ❌"
log " Decrease: $DELTA wei/token" log " Decrease: $DELTA wei"
log "" log ""
log " See $REPORT for the winning strategy." log " See $REPORT for the winning strategy."
log "" log ""
@ -639,24 +719,24 @@ if [[ "$BROKE" == "true" ]]; then
cat >>"$REPORT" <<SUMMARY_EOF cat >>"$REPORT" <<SUMMARY_EOF
=== RUNNER SUMMARY === === RUNNER SUMMARY ===
floor_before : $FLOOR_BEFORE lm_eth_before : $LM_ETH_BEFORE
floor_after : $FLOOR_AFTER lm_eth_after : $LM_ETH_AFTER
delta : -$DELTA delta : -$DELTA
verdict : FLOOR_BROKEN verdict : ETH_EXTRACTED
SUMMARY_EOF SUMMARY_EOF
exit 1 exit 1
else else
log " RESULT: FLOOR HELD ✅" log " RESULT: ETH SAFE ✅"
log "" log ""
log " See $REPORT for strategies attempted." log " See $REPORT for strategies attempted."
log "" log ""
cat >>"$REPORT" <<SUMMARY_EOF cat >>"$REPORT" <<SUMMARY_EOF
=== RUNNER SUMMARY === === RUNNER SUMMARY ===
floor_before : $FLOOR_BEFORE lm_eth_before : $LM_ETH_BEFORE
floor_after : $FLOOR_AFTER lm_eth_after : $LM_ETH_AFTER
delta : 0 (or increase) delta : 0 (or increase)
verdict : FLOOR_HELD verdict : ETH_SAFE
SUMMARY_EOF SUMMARY_EOF
exit 0 exit 0
fi fi