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:
parent
9832b454df
commit
0ddc1ccd80
3 changed files with 242 additions and 100 deletions
|
|
@ -52,6 +52,9 @@ when the protocol changes — not the marketing copy.
|
|||
- Staker governance for optimizer upgrades (vote with stake weight)
|
||||
- On-chain training data → new optimizer contracts via Push3 transpiler
|
||||
- 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
|
||||
|
||||
|
|
|
|||
59
onchain/script/LmTotalEth.s.sol
Normal file
59
onchain/script/LmTotalEth.s.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 jq &>/dev/null || die "jq not found"
|
||||
|
||||
# ── 1. Verify stack is running ─────────────────────────────────────────────────
|
||||
log "Verifying Anvil is accessible at $RPC_URL ..."
|
||||
# ── 1. Fresh stack — tear down, rebuild, wait for bootstrap ────────────────────
|
||||
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 \
|
||||
|| 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 ─────────────────────────────────────────────────
|
||||
[[ -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")
|
||||
log " Pool: $POOL"
|
||||
|
||||
# ── 3. Grant recenterAccess to account 2 ──────────────────────────────────────
|
||||
# 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) ..."
|
||||
# ── 3a. Grant recenterAccess FIRST (while original feeDestination is still set) ──
|
||||
FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \
|
||||
|| die "Failed to read feeDestination() from LM"
|
||||
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" \
|
||||
|| die "anvil_impersonateAccount $FEE_DEST failed"
|
||||
"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \
|
||||
"$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" \
|
||||
|| die "anvil_stopImpersonatingAccount $FEE_DEST failed"
|
||||
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) ─────
|
||||
log "Taking Anvil snapshot..."
|
||||
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
|
||||
|
|
@ -117,43 +185,18 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# ── Helper: compute ethPerToken (mirrors floor.ts getEthPerToken) ──────────────
|
||||
# ethPerToken = (lm_native_eth + lm_weth) * 1e18 / adjusted_outstanding_supply
|
||||
# adjusted_supply = outstandingSupply() - KRK_at_feeDestination - KRK_at_stakingPool
|
||||
compute_eth_per_token() {
|
||||
local lm_eth lm_weth supply fee_bal stake_bal adj_supply
|
||||
|
||||
lm_eth=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
lm_weth=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
supply=$("$CAST" call "$KRK" "outstandingSupply()(uint256)" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
|
||||
# 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: compute total ETH controlled by LM ────────────────────────────────
|
||||
# Total = free ETH + free WETH + ETH locked in all 3 Uni V3 positions
|
||||
# This is the real metric: "can the adversary extract ETH from the protocol?"
|
||||
# Uses a forge script with exact Uni V3 integer math (LiquidityAmounts + TickMath)
|
||||
# instead of multiple cast calls + Python float approximation.
|
||||
compute_lm_total_eth() {
|
||||
local output
|
||||
output=$(LM="$LM" WETH="$WETH" POOL="$POOL" \
|
||||
/home/debian/.foundry/bin/forge script script/LmTotalEth.s.sol \
|
||||
--rpc-url "$RPC_URL" --root "$REPO_ROOT/onchain" 2>&1)
|
||||
# forge script prints "== Logs ==" then " <value>" — extract the number
|
||||
echo "$output" | awk '/^== Logs ==/{getline; gsub(/^[[:space:]]+/,""); print; exit}'
|
||||
}
|
||||
|
||||
# ── Helper: extract strategy findings from stream-json and append to memory ────
|
||||
|
|
@ -169,7 +212,7 @@ extract_memory() {
|
|||
run_num=1
|
||||
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
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
|
@ -177,9 +220,9 @@ stream_file = sys.argv[1]
|
|||
memory_file = sys.argv[2]
|
||||
run_num = int(sys.argv[3])
|
||||
try:
|
||||
floor_before = int(sys.argv[4])
|
||||
lm_eth_before = int(sys.argv[4])
|
||||
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)
|
||||
|
||||
texts = []
|
||||
|
|
@ -209,7 +252,7 @@ for text in texts:
|
|||
current = {
|
||||
"strategy": strat_match.group(1).strip(),
|
||||
"steps": "",
|
||||
"floor_after": None,
|
||||
"lm_eth_after": None,
|
||||
"insight": ""
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +260,7 @@ for text in texts:
|
|||
# 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))
|
||||
if floor_matches:
|
||||
current["floor_after"] = int(floor_matches[-1].group(1))
|
||||
current["lm_eth_after"] = int(floor_matches[-1].group(1))
|
||||
|
||||
# Capture insights
|
||||
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()
|
||||
with open(memory_file, "a") as f:
|
||||
for s in strategies:
|
||||
fa = s["floor_after"] if s.get("floor_after") is not None else floor_before
|
||||
delta_bps = round((fa - floor_before) * 10000 / floor_before) if floor_before else 0
|
||||
if fa < floor_before:
|
||||
fa = s["lm_eth_after"] if s.get("lm_eth_after") is not None else lm_eth_before
|
||||
delta_bps = round((fa - lm_eth_before) * 10000 / lm_eth_before) if lm_eth_before else 0
|
||||
if fa < lm_eth_before:
|
||||
result = "DECREASED"
|
||||
elif fa > floor_before:
|
||||
elif fa > lm_eth_before:
|
||||
result = "INCREASED"
|
||||
else:
|
||||
result = "HELD"
|
||||
|
|
@ -251,8 +294,8 @@ with open(memory_file, "a") as f:
|
|||
"ts": ts,
|
||||
"strategy": s["strategy"][:100],
|
||||
"steps": s["steps"][:300].rstrip("; "),
|
||||
"floor_before": floor_before,
|
||||
"floor_after": fa,
|
||||
"lm_eth_before": lm_eth_before,
|
||||
"lm_eth_after": fa,
|
||||
"delta_bps": delta_bps,
|
||||
"result": result,
|
||||
"insight": s["insight"][:300]
|
||||
|
|
@ -284,10 +327,10 @@ if len(all_entries) > 50:
|
|||
PYEOF
|
||||
}
|
||||
|
||||
# ── 5. Read floor_before ───────────────────────────────────────────────────────
|
||||
# ── 5. Read lm_eth_before ───────────────────────────────────────────────────────
|
||||
log "Reading floor before agent run..."
|
||||
FLOOR_BEFORE=$(compute_eth_per_token)
|
||||
log " floor_before = $FLOOR_BEFORE wei/token"
|
||||
LM_ETH_BEFORE=$(compute_lm_total_eth)
|
||||
log " lm_eth_before = $LM_ETH_BEFORE wei"
|
||||
|
||||
# ── 6. Build agent prompt ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -323,15 +366,18 @@ PYEOF
|
|||
fi
|
||||
|
||||
PROMPT=$(cat <<PROMPT_EOF
|
||||
# Red-team mission: break the KRAIKEN protocol floor
|
||||
# Red-team mission: extract ETH from the KRAIKEN LiquidityManager
|
||||
|
||||
## 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
|
||||
It is the floor price — the minimum ETH per KRK token the protocol will pay.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -407,15 +453,38 @@ The floor formula then additionally subtracts KRK at Stake and feeDestination to
|
|||
|
||||
## 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
|
||||
LM_ETH=\$(/home/debian/.foundry/bin/cast balance ${LM} --rpc-url http://localhost:8545)
|
||||
LM_WETH=\$(/home/debian/.foundry/bin/cast call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545)
|
||||
SUPPLY=\$(/home/debian/.foundry/bin/cast call ${KRK} "outstandingSupply()(uint256)" --rpc-url http://localhost:8545)
|
||||
FEE_DEST=\$(/home/debian/.foundry/bin/cast call ${LM} "feeDestination()(address)" --rpc-url http://localhost:8545)
|
||||
FEE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" \$FEE_DEST --rpc-url http://localhost:8545)
|
||||
STAKE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${STAKE} --rpc-url http://localhost:8545)
|
||||
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')"
|
||||
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
|
||||
|
|
@ -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
|
||||
|
||||
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:
|
||||
a. Run the floor-check command above to measure ethPerToken.
|
||||
b. If the floor 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.
|
||||
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_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).
|
||||
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.
|
||||
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 ===
|
||||
|
||||
floor_before: <value> wei/token
|
||||
lm_eth_before: <value> wei (total: free + positions)
|
||||
|
||||
STRATEGY 1: <name>
|
||||
Steps: <what you did>
|
||||
floor_after: <value>
|
||||
Result: INCREASED / HELD / DECREASED
|
||||
lm_eth_after: <value> wei
|
||||
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
|
||||
|
||||
STRATEGY 2: ...
|
||||
...
|
||||
|
||||
=== CONCLUSION ===
|
||||
Floor broken: YES / NO
|
||||
ETH extracted: YES / NO
|
||||
Winning strategy: <describe if YES, else "None">
|
||||
floor_before: ${FLOOR_BEFORE} wei/token
|
||||
floor_after: <final value>
|
||||
lm_eth_before: ${LM_ETH_BEFORE} wei
|
||||
lm_eth_after: <final value> wei
|
||||
\`\`\`
|
||||
PROMPT_EOF
|
||||
)
|
||||
|
|
@ -601,37 +681,37 @@ with open(sys.argv[1]) as f:
|
|||
PYEOF
|
||||
|
||||
# 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
|
||||
die "claude agent failed (exit $AGENT_EXIT) with no readable output — see $STREAM_LOG"
|
||||
fi
|
||||
|
||||
# ── 8. Read floor_after ────────────────────────────────────────────────────────
|
||||
# ── 8. Read lm_eth_after ────────────────────────────────────────────────────────
|
||||
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 ──────────────────────────────────
|
||||
log "Extracting strategy findings from agent output..."
|
||||
extract_memory "$STREAM_LOG"
|
||||
log " floor_after = $FLOOR_AFTER wei/token"
|
||||
log " lm_eth_after = $LM_ETH_AFTER wei"
|
||||
|
||||
# ── 9. Summarise results ───────────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== RED-TEAM SUMMARY ==="
|
||||
log ""
|
||||
log " floor_before : $FLOOR_BEFORE wei/token"
|
||||
log " floor_after : $FLOOR_AFTER wei/token"
|
||||
log " lm_eth_before : $LM_ETH_BEFORE wei"
|
||||
log " lm_eth_after : $LM_ETH_AFTER wei"
|
||||
log ""
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
if [[ "$BROKE" == "true" ]]; then
|
||||
DELTA=$(python3 -c "print($FLOOR_BEFORE - $FLOOR_AFTER)")
|
||||
log " RESULT: FLOOR BROKEN ❌"
|
||||
log " Decrease: $DELTA wei/token"
|
||||
DELTA=$(python3 -c "print($LM_ETH_BEFORE - $LM_ETH_AFTER)")
|
||||
log " RESULT: ETH EXTRACTED ❌"
|
||||
log " Decrease: $DELTA wei"
|
||||
log ""
|
||||
log " See $REPORT for the winning strategy."
|
||||
log ""
|
||||
|
|
@ -639,24 +719,24 @@ if [[ "$BROKE" == "true" ]]; then
|
|||
cat >>"$REPORT" <<SUMMARY_EOF
|
||||
|
||||
=== RUNNER SUMMARY ===
|
||||
floor_before : $FLOOR_BEFORE
|
||||
floor_after : $FLOOR_AFTER
|
||||
lm_eth_before : $LM_ETH_BEFORE
|
||||
lm_eth_after : $LM_ETH_AFTER
|
||||
delta : -$DELTA
|
||||
verdict : FLOOR_BROKEN
|
||||
verdict : ETH_EXTRACTED
|
||||
SUMMARY_EOF
|
||||
exit 1
|
||||
else
|
||||
log " RESULT: FLOOR HELD ✅"
|
||||
log " RESULT: ETH SAFE ✅"
|
||||
log ""
|
||||
log " See $REPORT for strategies attempted."
|
||||
log ""
|
||||
cat >>"$REPORT" <<SUMMARY_EOF
|
||||
|
||||
=== RUNNER SUMMARY ===
|
||||
floor_before : $FLOOR_BEFORE
|
||||
floor_after : $FLOOR_AFTER
|
||||
lm_eth_before : $LM_ETH_BEFORE
|
||||
lm_eth_after : $LM_ETH_AFTER
|
||||
delta : 0 (or increase)
|
||||
verdict : FLOOR_HELD
|
||||
verdict : ETH_SAFE
|
||||
SUMMARY_EOF
|
||||
exit 0
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue