fix: bootstrap + red-team on forked networks
Bootstrap fixes:
- Idempotency check: skip if Kraiken already deployed on Anvil
- anvil_setCode to strip ERC-4337 code from deployer + feeDest
- DeployLocal.sol: feeDest derived from keccak256('harb.local.feeDest')
Red-team fixes:
- New bootstrap-light.sh: Anvil-only, ~30s deploy
- red-team.sh uses bootstrap-light instead of full docker compose
- anvil_setBalance for feeDest before impersonation
- forge --color never, path resolution, docker chown
Address fixes (all Base mainnet, in both FitnessEvaluator + AttackRunner):
- V3_FACTORY: 0x33128a8fC17869897dcE68Ed026d694621f6FDfD
- SWAP_ROUTER: 0x2626664c2603336E57B271c5C0b26F421741e481
- NPM_ADDR: 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1
This commit is contained in:
parent
8607c097eb
commit
dbf78de793
5 changed files with 208 additions and 61 deletions
|
|
@ -130,7 +130,33 @@ main() {
|
|||
|
||||
bootstrap_log "Waiting for Anvil"
|
||||
wait_for_rpc
|
||||
|
||||
# Idempotency: if deployments-local.json exists and contracts have code,
|
||||
# bootstrap already ran against this Anvil instance — skip.
|
||||
local deploy_file="$ONCHAIN_DIR/deployments-local.json"
|
||||
if [[ -f "$deploy_file" ]]; then
|
||||
local krk_addr
|
||||
krk_addr=$(jq -r '.contracts.Kraiken // empty' "$deploy_file" 2>/dev/null || true)
|
||||
if [[ -n "$krk_addr" ]]; then
|
||||
local code
|
||||
code=$(cast call --rpc-url "$ANVIL_RPC" "$krk_addr" "decimals()(uint8)" 2>/dev/null || true)
|
||||
if [[ -n "$code" && "$code" != "0x" ]]; then
|
||||
bootstrap_log "Already bootstrapped (Kraiken at $krk_addr responds) — skipping"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
maybe_set_deployer_from_mnemonic
|
||||
|
||||
# On forked networks, well-known addresses (Anvil mnemonic accounts) may
|
||||
# have code (e.g. ERC-4337 Account Abstraction proxies on Base Sepolia).
|
||||
# The feeDestination lock in LiquidityManager treats any address with code
|
||||
# as a contract and locks permanently. Strip code so they behave as EOAs.
|
||||
bootstrap_log "Clearing code from deployer + feeDest (fork safety)"
|
||||
cast rpc --rpc-url "$ANVIL_RPC" anvil_setCode "$DEPLOYER_ADDR" "0x" 2>/dev/null || true
|
||||
# feeDest = address(uint160(uint256(keccak256("harb.local.feeDest"))))
|
||||
cast rpc --rpc-url "$ANVIL_RPC" anvil_setCode "0x8A9145E1Ea4C4d7FB08cF1011c8ac1F0e10F9383" "0x" 2>/dev/null || true
|
||||
|
||||
derive_txnbot_wallet
|
||||
run_forge_script
|
||||
extract_addresses
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
|
|||
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
|
||||
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import "forge-std/Script.sol";
|
||||
import "./DeployCommon.sol";
|
||||
|
||||
/**
|
||||
* @title DeployLocal
|
||||
|
|
@ -23,10 +24,25 @@ contract DeployLocal is Script {
|
|||
uint24 internal constant FEE = uint24(10_000);
|
||||
|
||||
// Configuration
|
||||
address internal constant feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011;
|
||||
// Anvil account 9 — guaranteed to be an EOA with no code on any fork.
|
||||
// Previous address (0xf6a3...) has 171 bytes of code on Base mainnet,
|
||||
// which triggers the feeDestination lock.
|
||||
// Derived from keccak256 — guaranteed no code on any fork.
|
||||
address internal constant feeDest = address(uint160(uint256(keccak256("harb.local.feeDest"))));
|
||||
address internal constant weth = 0x4200000000000000000000000000000000000006;
|
||||
address internal constant v3Factory = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
|
||||
|
||||
// Seed amounts for VWAP bootstrap.
|
||||
// seedLmEth: initial ETH sent to the LM to create thin bootstrap positions.
|
||||
// seedSwapEth: ETH used for the seed buy. Must be large enough to move the
|
||||
// Uniswap tick >400 ticks past the ANCHOR center (minAmplitude = 2*tickSpacing
|
||||
// = 400 for the 1%-fee pool). The ANCHOR typically holds ~25% of seedLmEth as
|
||||
// WETH across a ~7200-tick range; consuming half of that WETH (≈0.125 ETH)
|
||||
// moves the price ~3600 ticks — well above the 400-tick threshold.
|
||||
// 0.5 ether provides a 4× margin over the minimum needed.
|
||||
uint256 internal constant SEED_LM_ETH = 1 ether;
|
||||
uint256 internal constant SEED_SWAP_ETH = 0.5 ether;
|
||||
|
||||
// Deployed contracts
|
||||
Kraiken public kraiken;
|
||||
Stake public stake;
|
||||
|
|
@ -48,7 +64,7 @@ contract DeployLocal is Script {
|
|||
|
||||
// Deploy Kraiken token
|
||||
kraiken = new Kraiken("Kraiken", "KRK");
|
||||
console.log("\n[1/6] Kraiken deployed:", address(kraiken));
|
||||
console.log("\n[1/7] Kraiken deployed:", address(kraiken));
|
||||
|
||||
// Determine token ordering
|
||||
token0isWeth = address(weth) < address(kraiken);
|
||||
|
|
@ -56,7 +72,7 @@ contract DeployLocal is Script {
|
|||
|
||||
// Deploy Stake contract
|
||||
stake = new Stake(address(kraiken), feeDest);
|
||||
console.log("\n[2/6] Stake deployed:", address(stake));
|
||||
console.log("\n[2/7] Stake deployed:", address(stake));
|
||||
|
||||
// Set staking pool in Kraiken
|
||||
kraiken.setStakingPool(address(stake));
|
||||
|
|
@ -67,9 +83,9 @@ contract DeployLocal is Script {
|
|||
address liquidityPool = factory.getPool(weth, address(kraiken), FEE);
|
||||
if (liquidityPool == address(0)) {
|
||||
liquidityPool = factory.createPool(weth, address(kraiken), FEE);
|
||||
console.log("\n[3/6] Uniswap pool created:", liquidityPool);
|
||||
console.log("\n[3/7] Uniswap pool created:", liquidityPool);
|
||||
} else {
|
||||
console.log("\n[3/6] Using existing pool:", liquidityPool);
|
||||
console.log("\n[3/7] Using existing pool:", liquidityPool);
|
||||
}
|
||||
pool = IUniswapV3Pool(liquidityPool);
|
||||
|
||||
|
|
@ -91,22 +107,73 @@ contract DeployLocal is Script {
|
|||
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake));
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params);
|
||||
address optimizerAddress = address(proxy);
|
||||
console.log("\n[4/6] Optimizer deployed:", optimizerAddress);
|
||||
console.log("\n[4/7] Optimizer deployed:", optimizerAddress);
|
||||
|
||||
// Deploy LiquidityManager
|
||||
liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress);
|
||||
console.log("\n[5/6] LiquidityManager deployed:", address(liquidityManager));
|
||||
console.log("\n[5/7] LiquidityManager deployed:", address(liquidityManager));
|
||||
|
||||
// Configure contracts
|
||||
kraiken.setLiquidityManager(address(liquidityManager));
|
||||
console.log(" LiquidityManager set in Kraiken");
|
||||
|
||||
// Set the real feeDestination.
|
||||
liquidityManager.setFeeDestination(feeDest);
|
||||
console.log("\n[6/7] Configuration complete");
|
||||
|
||||
console.log("\n[6/6] Configuration complete");
|
||||
console.log(" feeDestination set to", feeDest);
|
||||
console.log(" VWAP bootstrap will be performed by the bootstrap script");
|
||||
// =====================================================================
|
||||
// [7/7] VWAP Bootstrap -> seed trade during deployment
|
||||
//
|
||||
// The cumulativeVolume==0 path in recenter() records VWAP from whatever
|
||||
// price exists at the time of the first fee event. An attacker who
|
||||
// front-runs deployment with a whale buy inflates that anchor.
|
||||
//
|
||||
// Fix: execute a small buy BEFORE handing control to users so that
|
||||
// cumulativeVolume>0 by the time the protocol is live.
|
||||
//
|
||||
// Sequence:
|
||||
// 1. Temporarily make sender the feeDestination (deployer can do this
|
||||
// because setFeeDestination is gated on deployer, not feeDestination).
|
||||
// This allows sender to call setRecenterAccess.
|
||||
// 2. Fund LM with SEED_LM_ETH and call recenter() -> places thin initial
|
||||
// positions; no fees collected yet, so cumulativeVolume stays 0.
|
||||
// 3. Execute seed buy via SeedSwapper -> generates a non-zero WETH fee
|
||||
// in the anchor position and moves the tick >400 (minimum amplitude).
|
||||
// 4. Call recenter() again -> cumulativeVolume==0 triggers the bootstrap
|
||||
// path (shouldRecordVWAP=true); ethFee>0 → _recordVolumeAndPrice fires
|
||||
// → cumulativeVolume>0. VWAP is now anchored to the real launch price.
|
||||
// 5. Revoke recenterAccess and restore the real feeDestination.
|
||||
// =====================================================================
|
||||
console.log("\n[7/7] Bootstrapping VWAP with seed trade...");
|
||||
|
||||
// Step 1: Grant deployer temporary feeDestination role to enable setRecenterAccess.
|
||||
// NOTE: on forked networks, bootstrap.sh pre-clears code from deployer
|
||||
// and feeDest via anvil_setCode — required because Base Sepolia has
|
||||
// ERC-4337 code at well-known addresses, triggering feeDestination lock.
|
||||
liquidityManager.setFeeDestination(sender);
|
||||
liquidityManager.setRecenterAccess(sender);
|
||||
console.log(" Temporary recenterAccess granted to deployer");
|
||||
|
||||
// Step 2: Fund LM and place initial bootstrap positions.
|
||||
(bool funded,) = address(liquidityManager).call{ value: SEED_LM_ETH }("");
|
||||
require(funded, "Failed to fund LM for seed bootstrap");
|
||||
liquidityManager.recenter();
|
||||
console.log(" First recenter complete -> positions placed, cumulativeVolume still 0");
|
||||
|
||||
// Step 3: Seed buy -> generates a non-zero fee in the anchor position.
|
||||
SeedSwapper seedSwapper = new SeedSwapper(weth, address(pool), token0isWeth);
|
||||
seedSwapper.executeSeedBuy{ value: SEED_SWAP_ETH }(sender);
|
||||
console.log(" Seed buy executed -> fee generated in anchor position");
|
||||
|
||||
// Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0).
|
||||
liquidityManager.recenter();
|
||||
require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0");
|
||||
console.log(" Second recenter complete -> VWAP bootstrapped");
|
||||
console.log(" cumulativeVolume:", liquidityManager.cumulativeVolume());
|
||||
console.log(" VWAP (X96):", liquidityManager.getVWAP());
|
||||
|
||||
// Step 5: Clean up -> revoke temporary access and set the real feeDestination.
|
||||
liquidityManager.revokeRecenterAccess();
|
||||
liquidityManager.setFeeDestination(feeDest);
|
||||
console.log(" recenterAccess revoked, feeDestination restored to", feeDest);
|
||||
|
||||
// Print deployment summary
|
||||
console.log("\n=== Deployment Summary ===");
|
||||
|
|
@ -117,11 +184,12 @@ contract DeployLocal is Script {
|
|||
console.log("Optimizer:", optimizerAddress);
|
||||
|
||||
console.log("\n=== Next Steps ===");
|
||||
console.log("1. bootstrap-common.sh bootstrap_vwap() advances chain time and seeds VWAP.");
|
||||
console.log("2. Fund LiquidityManager with operational ETH:");
|
||||
console.log("VWAP is already bootstrapped. To go live:");
|
||||
console.log("1. Fund LiquidityManager with operational ETH (current balance includes seed):");
|
||||
console.log(" cast send", address(liquidityManager), "--value 10ether");
|
||||
console.log("3. recenter() is permissionless - any address (e.g. txnBot) can call it.");
|
||||
console.log(" TWAP manipulation protection is always enforced (no bypass path).");
|
||||
console.log("2. Grant recenterAccess to txnBot (call from feeDestination):");
|
||||
console.log(" cast send", address(liquidityManager), "\"setRecenterAccess(address)\" <txnBotAddr>");
|
||||
console.log("3. txnBot can now call recenter() to rebalance positions.");
|
||||
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,11 +151,9 @@ contract AttackRunner is Script {
|
|||
|
||||
uint24 internal constant POOL_FEE = 10_000;
|
||||
address internal constant WETH = 0x4200000000000000000000000000000000000006;
|
||||
address internal constant SWAP_ROUTER = 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4;
|
||||
// Base mainnet NonfungiblePositionManager — https://basescan.org/address/0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3
|
||||
address internal constant NPM_ADDR = 0x03a520B32c04bf3beef7BEb72E919cF822Ed34F3;
|
||||
// Base mainnet Uniswap V3 Factory — https://basescan.org/address/0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24
|
||||
address internal constant V3_FACTORY = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
|
||||
address internal constant SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;
|
||||
address internal constant NPM_ADDR = 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1;
|
||||
address internal constant V3_FACTORY = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD; // Base mainnet
|
||||
|
||||
// ─── Anvil test accounts ──────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
65
scripts/harb-evaluator/bootstrap-light.sh
Executable file
65
scripts/harb-evaluator/bootstrap-light.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env bash
|
||||
# Lightweight bootstrap for red-team / evaluator use.
|
||||
# Starts only Anvil + deploys contracts. No ponder, no webapp, no txnbot.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
ONCHAIN_DIR="$REPO_ROOT/onchain"
|
||||
RPC_URL="http://localhost:8545"
|
||||
CAST="$HOME/.foundry/bin/cast"
|
||||
FORGE="$HOME/.foundry/bin/forge"
|
||||
|
||||
log() { echo "[bootstrap-light] $*"; }
|
||||
die() { log "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
# 1. Start Anvil (docker)
|
||||
log "Starting Anvil..."
|
||||
cd "$REPO_ROOT"
|
||||
sudo docker compose down -v 2>/dev/null || true
|
||||
sudo docker compose up -d anvil
|
||||
for i in $(seq 1 30); do
|
||||
$CAST chain-id --rpc-url "$RPC_URL" 2>/dev/null && break
|
||||
sleep 1
|
||||
done
|
||||
$CAST chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 || die "Anvil not responding"
|
||||
log "Anvil running"
|
||||
|
||||
# 2. Clear ERC-4337 code from well-known addresses (fork safety)
|
||||
DEPLOYER=$($CAST wallet address --mnemonic "test test test test test test test test test test test junk" 2>/dev/null)
|
||||
log "Clearing code from deployer ($DEPLOYER) + feeDest"
|
||||
$CAST rpc --rpc-url "$RPC_URL" anvil_setCode "$DEPLOYER" "0x" 2>/dev/null || true
|
||||
$CAST rpc --rpc-url "$RPC_URL" anvil_setCode "0x8A9145E1Ea4C4d7FB08cF1011c8ac1F0e10F9383" "0x" 2>/dev/null || true
|
||||
|
||||
# 3. Deploy contracts — capture output for addresses
|
||||
log "Deploying contracts..."
|
||||
cd "$ONCHAIN_DIR"
|
||||
# Fix ownership of forge artifacts (docker creates root-owned files)
|
||||
sudo chown -R "$(id -u):$(id -g)" cache out broadcast 2>/dev/null || true
|
||||
rm -f deployments-local.json # force fresh
|
||||
DEPLOY_OUT=$($FORGE script script/DeployLocal.sol --rpc-url "$RPC_URL" --broadcast 2>&1)
|
||||
echo "$DEPLOY_OUT" | grep -E "^\[|deployed|complete|Summary" || true
|
||||
|
||||
# 4. Extract addresses from output and write deployments-local.json
|
||||
KRK=$(echo "$DEPLOY_OUT" | grep -oP 'Kraiken deployed: \K0x[a-fA-F0-9]+')
|
||||
STAKE=$(echo "$DEPLOY_OUT" | grep -oP 'Stake deployed: \K0x[a-fA-F0-9]+')
|
||||
OPT=$(echo "$DEPLOY_OUT" | grep -oP 'Optimizer deployed: \K0x[a-fA-F0-9]+')
|
||||
LM=$(echo "$DEPLOY_OUT" | grep -oP 'LiquidityManager deployed: \K0x[a-fA-F0-9]+')
|
||||
|
||||
[[ -n "$LM" ]] || die "Could not extract LiquidityManager address from deploy output"
|
||||
|
||||
cat > "$ONCHAIN_DIR/deployments-local.json" << EOF
|
||||
{
|
||||
"contracts": {
|
||||
"Kraiken": "$KRK",
|
||||
"Stake": "$STAKE",
|
||||
"LiquidityManager": "$LM",
|
||||
"OptimizerProxy": "$OPT"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# 5. Verify
|
||||
VWAP=$($CAST call --rpc-url "$RPC_URL" "$LM" "cumulativeVolume()(uint256)" 2>/dev/null || echo "0")
|
||||
log "LiquidityManager: $LM"
|
||||
log "cumulativeVolume: $VWAP"
|
||||
[[ "$VWAP" != "0" ]] && log "✅ Bootstrap complete — VWAP active" || log "⚠️ VWAP not bootstrapped"
|
||||
|
|
@ -33,7 +33,7 @@ DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json"
|
|||
# ── Anvil accounts ─────────────────────────────────────────────────────────────
|
||||
# Account 8 — adversary (10k ETH, 0 KRK)
|
||||
ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
|
||||
# Account 2 — recenter caller (recenter() is permissionless; any account can call it)
|
||||
# Account 2 — recenter caller (granted recenterAccess by bootstrap)
|
||||
RECENTER_PK=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
|
||||
|
||||
# ── Infrastructure constants ───────────────────────────────────────────────────
|
||||
|
|
@ -55,39 +55,14 @@ 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. 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 -E docker compose down -v >/dev/null 2>&1 || true
|
||||
sleep 3
|
||||
|
||||
# Bring up
|
||||
# -E preserves FORK_URL (and other env vars) across the sudo boundary so that
|
||||
# anvil-entrypoint.sh honours the caller's FORK_URL override.
|
||||
sudo -E 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
|
||||
# ── 1. Fresh stack via bootstrap-light ─────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
log "Running bootstrap-light ..."
|
||||
bash "$SCRIPT_DIR/bootstrap-light.sh" || die "bootstrap-light failed"
|
||||
|
||||
# Verify Anvil responds
|
||||
"$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
||||
|| die "Anvil not accessible at $RPC_URL after stack start"
|
||||
|| die "Anvil not accessible at $RPC_URL after bootstrap-light"
|
||||
|
||||
# ── 2. Read contract addresses ─────────────────────────────────────────────────
|
||||
[[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)"
|
||||
|
|
@ -119,8 +94,23 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \
|
|||
"$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
log " Pool: $POOL"
|
||||
|
||||
# ── 3a. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
|
||||
# recenter() is now permissionless — no setRecenterAccess() call needed.
|
||||
# ── 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" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) via feeDestination ($FEE_DEST) ..."
|
||||
# feeDest may be a keccak-derived address with zero balance — fund it for gas
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_setBalance "$FEE_DEST" "0xDE0B6B3A7640000" 2>/dev/null || true
|
||||
"$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"
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \
|
||||
|| die "anvil_stopImpersonatingAccount $FEE_DEST failed"
|
||||
log " recenterAccess granted"
|
||||
|
||||
# ── 3b. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
|
||||
# setFeeDestination allows repeated EOA sets; setting to a contract locks it permanently.
|
||||
# The deployer (Anvil account 0) deployed LiquidityManager and may call setFeeDestination again.
|
||||
# DEPLOYER_PK is Anvil's deterministic account-0 key — valid ONLY against a local ephemeral
|
||||
|
|
@ -134,7 +124,7 @@ VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | s
|
|||
log " feeDestination set to: $VERIFY"
|
||||
[[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY"
|
||||
|
||||
# ── 3b. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
|
||||
# ── 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 ..."
|
||||
|
|
@ -164,7 +154,7 @@ LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]
|
|||
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) ────────────────────────────────────
|
||||
# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ─────
|
||||
log "Taking Anvil snapshot..."
|
||||
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
|
||||
log " Snapshot ID: $SNAP"
|
||||
|
|
@ -190,9 +180,9 @@ trap cleanup EXIT INT TERM
|
|||
# instead of multiple cast calls + Python float approximation.
|
||||
compute_lm_total_eth() {
|
||||
local output result
|
||||
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" --no-color 2>&1)
|
||||
output=$(cd "$REPO_ROOT" && LM="$LM" WETH="$WETH" POOL="$POOL" \
|
||||
"$FORGE" script onchain/script/LmTotalEth.s.sol \
|
||||
--rpc-url "$RPC_URL" --root onchain 2>&1)
|
||||
# forge script prints "== Logs ==" then " <value>" — extract the number
|
||||
result=$(echo "$output" | awk '/^== Logs ==/{getline; gsub(/^[[:space:]]+/,""); print; exit}')
|
||||
[[ -n "$result" && "$result" =~ ^[0-9]+$ ]] || die "Failed to read LM total ETH (forge output: $output)"
|
||||
|
|
@ -409,7 +399,7 @@ CAST binary: /home/debian/.foundry/bin/cast
|
|||
### Recenter caller — Anvil account 2
|
||||
- Address: ${RECENTER_ADDR}
|
||||
- Private key: ${RECENTER_PK}
|
||||
- Can call recenter() (permissionless — 60s cooldown + TWAP check enforced)
|
||||
- Has recenterAccess on LiquidityManager
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -436,7 +426,7 @@ 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
|
||||
recenter() is permissionless — any account can call it (subject to 60s cooldown and TWAP check).
|
||||
Only recenterAccess account can call it.
|
||||
|
||||
### Staking
|
||||
\`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue