292 lines
10 KiB
Bash
Executable File
292 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Plan + verify Chain 138 → 10 networks: move 75% of each c* balance split evenly (7.5% per network).
|
||
# Uses CWMultiTokenBridgeL1 on 138 (CW_L1_BRIDGE_CHAIN138) when routes are configured.
|
||
#
|
||
# Modes:
|
||
# --plan-only Write JSON table + human summary (default)
|
||
# --check-routes cast call supportedCanonicalToken + destinations per token×chain
|
||
# --emit-cmds Print approve + lockAndSend cast lines (dry; review before running)
|
||
# --help
|
||
#
|
||
# Env: PRIVATE_KEY, RPC_URL_138, CW_L1_BRIDGE_CHAIN138, smom-dbis-138/.env
|
||
# Optional: RECIPIENT_ADDRESS (default: deployer), OUT_JSON (default: reports/status/c138-bridge-75-split-latest.json)
|
||
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||
PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
|
||
cd "$SMOM_ROOT"
|
||
|
||
MODE="plan"
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--plan-only) MODE="plan" ;;
|
||
--check-routes) MODE="check" ;;
|
||
--emit-cmds) MODE="emit" ;;
|
||
--help|-h)
|
||
grep '^#' "$0" | head -20
|
||
exit 0
|
||
;;
|
||
*) echo "Unknown: $1"; exit 1 ;;
|
||
esac
|
||
shift || true
|
||
done
|
||
|
||
if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
|
||
# shellcheck disable=SC1090
|
||
PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
|
||
elif [[ -f .env ]]; then
|
||
set -a && source .env && set +a
|
||
fi
|
||
|
||
RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}"
|
||
OUT_JSON="${OUT_JSON:-$SMOM_ROOT/reports/status/c138-bridge-75-split-latest.json}"
|
||
BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
|
||
DEPLOYER=""
|
||
if [[ -n "${PRIVATE_KEY:-}" ]]; then
|
||
DEPLOYER="$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true)"
|
||
fi
|
||
RECIPIENT="${RECIPIENT_ADDRESS:-$DEPLOYER}"
|
||
export RPC OUT_JSON DEPLOYER RECIPIENT BRIDGE
|
||
|
||
# CCIP chain selectors (Chainlink CCIP mainnet directory / repo BRIDGE_CONFIGURATION.md). Verify before prod.
|
||
declare -A SELECTOR=(
|
||
[Mainnet]=5009297550715157269
|
||
[Optimism]=3734403246176062136
|
||
[Cronos]=1456215246176062136
|
||
[BSC]=11344663589394136015
|
||
[Gnosis]=465200170687744372
|
||
[Polygon]=4051577828743386545
|
||
[Base]=15971525489660198786
|
||
[Arbitrum]=4949039107694359620
|
||
[Celo]=1346049177634351622
|
||
[Avalanche]=6433500567565415381
|
||
)
|
||
|
||
# name:address (Compliant / canonical c* on 138)
|
||
read -r -d '' TOKEN_ROWS << 'EOF' || true
|
||
cUSDT:0x93E66202A11B1772E55407B32B44e5Cd8eda7f22
|
||
cUSDC:0xf22258f57794CC8E06237084b353Ab30fFfa640b
|
||
cUSDT_V2:0x8d342d321DdEe97D0c5011DAF8ca0B59DA617D29
|
||
cUSDC_V2:0x1ac3F4942a71E86A9682D91837E1E71b7BACdF99
|
||
cEURC:0x8085961F9cF02b4d800A3c6d386D31da4B34266a
|
||
cEURT:0xdf4b71c61E5912712C1Bdd451416B9aC26949d72
|
||
cGBPC:0x003960f16D9d34F2e98d62723B6721Fb92074aD2
|
||
cGBPT:0x350f54e4D23795f86A9c03988c7135357CCaD97c
|
||
cAUDC:0xD51482e567c03899eecE3CAe8a058161FD56069D
|
||
cJPYC:0xEe269e1226a334182aace90056EE4ee5Cc8A6770
|
||
cCHFC:0x873990849DDa5117d7C644f0aF24370797C03885
|
||
cCADC:0x54dBd40cF05e15906A2C21f600937e96787f5679
|
||
cAUDT:0xC034b8Ff3088f644D492E95619720ba8fB582933
|
||
cJPYT:0x54fb3A6b16163D8cFa48EAff79205D1309B1a9A1
|
||
cCHFT:0xd91f31725444dD1F53FA6dE236A5e90a8281d970
|
||
cCADT:0xAb456be5Db1E1069F55F75E8c8fecAa6a71D1c8F
|
||
cXAUC:0x290E52a8819A4fbD0714E517225429aA2B70EC6b
|
||
cXAUT:0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E
|
||
cAUSDT:0x5fdDF65733e3d590463F68f93Cf16E8c04081271
|
||
EOF
|
||
|
||
export TOKEN_ROWS
|
||
mkdir -p "$(dirname "$OUT_JSON")"
|
||
|
||
python3 << PY
|
||
import json, subprocess, os, re, sys
|
||
|
||
rpc = os.environ.get("RPC", "http://192.168.11.211:8545")
|
||
deployer = os.environ.get("DEPLOYER", "")
|
||
recipient = os.environ.get("RECIPIENT", deployer)
|
||
bridge = os.environ.get("BRIDGE", "")
|
||
|
||
selectors = {
|
||
"Mainnet": 5009297550715157269,
|
||
"Optimism": 3734403246176062136,
|
||
"Cronos": 1456215246176062136,
|
||
"BSC": 11344663589394136015,
|
||
"Gnosis": 465200170687744372,
|
||
"Polygon": 4051577828743386545,
|
||
"Base": 15971525489660198786,
|
||
"Arbitrum": 4949039107694359620,
|
||
"Celo": 1346049177634351622,
|
||
"Avalanche": 6433500567565415381,
|
||
}
|
||
n_chains = len(selectors)
|
||
|
||
rows = []
|
||
for line in os.environ.get("TOKEN_ROWS", "").strip().split("\n"):
|
||
if not line.strip() or line.startswith("#"):
|
||
continue
|
||
sym, addr = line.split(":", 1)
|
||
rows.append((sym.strip(), addr.strip()))
|
||
|
||
def balance_of(addr):
|
||
if not deployer:
|
||
return None
|
||
r = subprocess.run(
|
||
["cast", "call", addr, "balanceOf(address)(uint256)", deployer, "--rpc-url", rpc],
|
||
capture_output=True, text=True,
|
||
)
|
||
if r.returncode != 0:
|
||
return None
|
||
m = re.match(r"^\s*(\d+)", r.stdout.strip())
|
||
return int(m.group(1)) if m else None
|
||
|
||
plan = {
|
||
"schema": "c138-bridge-75-split/v1",
|
||
"rpc_url": rpc,
|
||
"deployer": deployer,
|
||
"recipient": recipient,
|
||
"cw_l1_bridge": bridge,
|
||
"split": "75% of balance divided evenly across 10 networks (7.5% per chain, integer base units)",
|
||
"tokens": [],
|
||
}
|
||
|
||
for sym, addr in rows:
|
||
bal = balance_of(addr)
|
||
if bal is None:
|
||
plan["tokens"].append({"symbol": sym, "address": addr, "error": "balance_of_failed"})
|
||
continue
|
||
q75 = bal * 75 // 100
|
||
per = q75 // n_chains
|
||
rem = q75 % n_chains
|
||
entry = {
|
||
"symbol": sym,
|
||
"address": addr,
|
||
"balance_wei": str(bal),
|
||
"pct_75_wei": str(q75),
|
||
"per_chain_wei": str(per),
|
||
"remainder_wei": str(rem),
|
||
"chains": {},
|
||
}
|
||
for cname, sel in selectors.items():
|
||
entry["chains"][cname] = {"selector": str(sel), "amount_wei": str(per)}
|
||
plan["tokens"].append(entry)
|
||
|
||
path = os.environ.get("OUT_JSON", "")
|
||
with open(path, "w") as f:
|
||
json.dump(plan, f, indent=2)
|
||
|
||
print(json.dumps({"written": path, "tokens": len(plan["tokens"])}))
|
||
PY
|
||
|
||
export RPC DEPLOYER RECIPIENT BRIDGE OUT_JSON
|
||
export TOKEN_ROWS="$TOKEN_ROWS"
|
||
|
||
python3 - <<'PY'
|
||
import json, os
|
||
with open(os.environ["OUT_JSON"]) as f:
|
||
p = json.load(f)
|
||
print("\n=== c* 75% / 10 networks (per-chain amount, 6 dp human for fiat-style) ===\n")
|
||
print(f"Deployer: {p.get('deployer','?')}\nRecipient: {p.get('recipient','?')}\nBridge: {p.get('cw_l1_bridge') or '(unset)'}\n")
|
||
for t in p["tokens"]:
|
||
if "error" in t:
|
||
print(f"{t['symbol']}: {t['error']}")
|
||
continue
|
||
sym = t["symbol"]
|
||
per = int(t["per_chain_wei"])
|
||
if sym.startswith("cXAU"):
|
||
hu = per / 1e6
|
||
print(f"{sym:<10} per chain: {hu:,.6f} troy oz (wei={t['per_chain_wei']})")
|
||
else:
|
||
hu = per / 1e6
|
||
print(f"{sym:<10} per chain: {hu:,.6f} tokens (wei={t['per_chain_wei']})")
|
||
print("\nJSON:", os.environ["OUT_JSON"])
|
||
PY
|
||
|
||
if [[ "$MODE" == "plan" ]]; then
|
||
exit 0
|
||
fi
|
||
|
||
[[ -n "$BRIDGE" ]] || { echo "Set CW_L1_BRIDGE_CHAIN138"; exit 1; }
|
||
code=$(cast code "$BRIDGE" --rpc-url "$RPC" 2>/dev/null || echo "0x")
|
||
[[ -n "$code" && "$code" != "0x" ]] || { echo "No contract at CW_L1_BRIDGE_CHAIN138=$BRIDGE"; exit 1; }
|
||
|
||
if [[ "$MODE" == "check" ]]; then
|
||
echo ""
|
||
echo "=== Route checks: $BRIDGE ==="
|
||
while IFS= read -r line; do
|
||
[[ -z "$line" ]] && continue
|
||
sym="${line%%:*}"
|
||
addr="${line#*:}"
|
||
if sup_raw=$(cast call "$BRIDGE" "supportedCanonicalToken(address)(bool)" "$addr" --rpc-url "$RPC" 2>/dev/null); then
|
||
echo "$sym supported=$sup_raw"
|
||
else
|
||
echo "$sym supported=(query reverted — non-CW ABI or older build; rely on destinations below)"
|
||
fi
|
||
for net in Mainnet Optimism Cronos BSC Gnosis Polygon Base Arbitrum Celo Avalanche; do
|
||
sel="${SELECTOR[$net]}"
|
||
dest=$(cast call "$BRIDGE" "destinations(address,uint64)(address,bool)" "$addr" "$sel" --rpc-url "$RPC" 2>/dev/null || echo "ERR")
|
||
echo " $net ($sel): $dest"
|
||
done
|
||
done <<< "$TOKEN_ROWS"
|
||
exit 0
|
||
fi
|
||
|
||
if [[ "$MODE" == "emit" ]]; then
|
||
[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required for --emit-cmds"; exit 1; }
|
||
[[ -n "$RECIPIENT" ]] || { echo "RECIPIENT_ADDRESS or deployer required"; exit 1; }
|
||
LINK_TOKEN="${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}"
|
||
[[ -n "$LINK_TOKEN" ]] || { echo "Set LINK_TOKEN or LINK_TOKEN_CHAIN138 for fee approval lines"; exit 1; }
|
||
export LINK_TOKEN
|
||
echo ""
|
||
echo "=== Review-only cast snippets (feeToken=LINK on this bridge: approve LINK, approve token, lockAndSend) ==="
|
||
OUT_CAST="${OUT_CAST:-$SMOM_ROOT/reports/status/c138-bridge-75-split-cast-commands.sh}"
|
||
export OUT_CAST
|
||
{
|
||
echo "#!/usr/bin/env bash"
|
||
echo "# Generated: c138-cw-bridge-75-split.sh --emit-cmds"
|
||
echo "# Review destinations + reserve verifier before running. Fund LINK for fees."
|
||
echo "set -euo pipefail"
|
||
# Quoted heredoc: do not let bash expand \$PRIVATE_KEY into the generated file.
|
||
python3 <<'PY'
|
||
import json, os, subprocess
|
||
rpc = os.environ["RPC"]
|
||
bridge = os.environ["BRIDGE"]
|
||
recipient = os.environ["RECIPIENT"]
|
||
link = os.environ["LINK_TOKEN"]
|
||
with open(os.environ["OUT_JSON"]) as f:
|
||
plan = json.load(f)
|
||
selectors = {
|
||
"Mainnet": 5009297550715157269,
|
||
"Optimism": 3734403246176062136,
|
||
"Cronos": 1456215246176062136,
|
||
"BSC": 11344663589394136015,
|
||
"Gnosis": 465200170687744372,
|
||
"Polygon": 4051577828743386545,
|
||
"Base": 15971525489660198786,
|
||
"Arbitrum": 4949039107694359620,
|
||
"Celo": 1346049177634351622,
|
||
"Avalanche": 6433500567565415381,
|
||
}
|
||
for t in plan["tokens"]:
|
||
if "error" in t or int(t.get("per_chain_wei", 0)) == 0:
|
||
continue
|
||
sym, token, amt = t["symbol"], t["address"], t["per_chain_wei"]
|
||
for cname, sel in selectors.items():
|
||
chk = subprocess.run(
|
||
["cast", "call", bridge, "destinations(address,uint64)(address,bool)", token, str(sel), "--rpc-url", rpc],
|
||
capture_output=True, text=True,
|
||
)
|
||
if chk.returncode != 0 or "true" not in chk.stdout:
|
||
continue
|
||
fee = subprocess.run(
|
||
["cast", "call", bridge,
|
||
"calculateFee(address,uint64,address,uint256)(uint256)",
|
||
token, str(sel), recipient, amt,
|
||
"--rpc-url", rpc],
|
||
capture_output=True, text=True,
|
||
)
|
||
fq = fee.stdout.strip().split()[0] if fee.returncode == 0 else "0"
|
||
print("")
|
||
print(f"# {sym} -> {cname} selector={sel} amount={amt} fee_wei={fq}")
|
||
print(f"cast send {link} \"approve(address,uint256)\" {bridge} {fq} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 120000")
|
||
print(f"cast send {token} \"approve(address,uint256)\" {bridge} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 400000")
|
||
print(f"cast send {bridge} \"lockAndSend(address,uint64,address,uint256)\" {token} {sel} {recipient} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 4000000")
|
||
PY
|
||
} > "$OUT_CAST"
|
||
chmod +x "$OUT_CAST"
|
||
echo "Wrote enabled-routes-only commands: $OUT_CAST"
|
||
wc -l "$OUT_CAST"
|
||
exit 0
|
||
fi
|