#!/usr/bin/env bash # Mint Ethereum mainnet cWUSDC directly to each address in config/pmm-soak-wallet-grid.json # (EI matrix). Requires PRIVATE_KEY with MINTER_ROLE on the cWUSDC token. # # Modes (exactly one): # --mint-raw R Same raw units minted to every wallet in the slice. # --total-mint-raw B Total supply to mint across the slice, split with ±spread # then renormalized to B (same algorithm as transfer distribution). # # Usage: # ./scripts/deployment/mint-cwusdc-ei-matrix-wallets.sh [--dry-run] [--limit N] [--offset N|--resume-next] # (--mint-raw R | --total-mint-raw B [--spread-pct S]) # # --quiet-dry-run With --dry-run, suppress per-wallet lines. # --legacy Pass --legacy to cast send. # # Env: ETHEREUM_MAINNET_RPC, CWUSDC_MAINNET, PRIVATE_KEY, # EI_MATRIX_MINT_GAS_EST (default 60000), EI_MATRIX_GAS_HEADROOM_BPS (default 10500), # EI_MATRIX_SKIP_GAS_CHECK=1 to bypass ETH preflight. # # Progress: reports/status/ei-matrix-cwusdc-mint-last-idx.txt # Failures: reports/status/ei-matrix-cwusdc-mint-failures.log # Lock: reports/status/ei-matrix-cwusdc-mint.lock # # On-chain: cWUSDC uses CompliantWrappedToken-style mint(address,uint256) for MINTER_ROLE. # If mint reverts (reserve policy, roles), fix on-chain state before retrying. # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$PROJECT_ROOT" DRY_RUN=false LIMIT="" OFFSET="0" OFFSET_EXPLICIT=false RESUME_NEXT=false SPREAD_PCT="${EI_MATRIX_SPREAD_PCT:-15}" CAST_LEGACY=false QUIET_DRY_RUN=false MINT_RAW="" TOTAL_MINT_RAW="" while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true; shift ;; --quiet-dry-run) QUIET_DRY_RUN=true; shift ;; --limit) LIMIT="${2:?}"; shift 2 ;; --resume-next) RESUME_NEXT=true; shift ;; --offset) OFFSET="${2:?}"; OFFSET_EXPLICIT=true; shift 2 ;; --spread-pct) SPREAD_PCT="${2:?}"; shift 2 ;; --mint-raw) MINT_RAW="${2:?}"; shift 2 ;; --total-mint-raw) TOTAL_MINT_RAW="${2:?}"; shift 2 ;; --legacy) CAST_LEGACY=true; shift ;; *) echo "Unknown arg: $1" >&2; exit 1 ;; esac done LAST_IDX_FILE="${EI_MATRIX_CWUSDC_MINT_LAST_IDX_FILE:-${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint-last-idx.txt}" if $RESUME_NEXT && $OFFSET_EXPLICIT; then echo "Use only one of --offset or --resume-next." >&2 exit 1 fi if $RESUME_NEXT; then [[ -f "$LAST_IDX_FILE" ]] || { echo "Missing last-index file for --resume-next: $LAST_IDX_FILE" >&2; exit 1; } _last="$(tr -d '[:space:]' < "$LAST_IDX_FILE" || echo "")" [[ -n "$_last" ]] || { echo "Empty $LAST_IDX_FILE" >&2; exit 1; } OFFSET=$((_last + 1)) echo "Resume-next (mint): last completed idx=$_last → offset=$OFFSET" fi if [[ -n "$MINT_RAW" && -n "$TOTAL_MINT_RAW" ]]; then echo "Use only one of --mint-raw or --total-mint-raw." >&2 exit 1 fi if [[ -z "$MINT_RAW" && -z "$TOTAL_MINT_RAW" ]]; then echo "Set --mint-raw or --total-mint-raw." >&2 exit 1 fi # shellcheck disable=SC1091 source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" CWUSDC="${CWUSDC_MAINNET:-0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a}" PUBLIC_ETH_RPC="${ETHEREUM_MAINNET_PUBLIC_RPC:-https://ethereum-rpc.publicnode.com}" RPC="${ETHEREUM_MAINNET_RPC:-${RPC_URL_1:-${ETH_MAINNET_RPC_URL:-$PUBLIC_ETH_RPC}}}" BALANCE_RPC="${EI_MATRIX_BALANCE_RPC:-$RPC}" LOCK_FILE="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint.lock" MANIFEST_DIR="${PROJECT_ROOT}/reports/status" mkdir -p "$MANIFEST_DIR" exec 200>"$LOCK_FILE" if ! flock -n 200; then echo "Another mint-cwusdc-ei-matrix-wallets.sh is already running (lock: $LOCK_FILE)." >&2 exit 1 fi GRID="$PROJECT_ROOT/config/pmm-soak-wallet-grid.json" DEPLOYER_CANONICAL="0x4A666F96fC8764181194447A7dFdb7d471b301C8" [[ -f "$GRID" ]] || { echo "Missing $GRID" >&2; exit 1; } command -v cast &>/dev/null || { echo "cast required" >&2; exit 1; } command -v jq &>/dev/null || { echo "jq required" >&2; exit 1; } [[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY not set" >&2; exit 1; } FROM_ADDR=$(cast wallet address --private-key "$PRIVATE_KEY") CHAIN_ID=$(cast chain-id --rpc-url "$RPC" 2>/dev/null | tr -d '[:space:]' || true) [[ -n "$CHAIN_ID" ]] || CHAIN_ID="1" if [[ "$CHAIN_ID" != "1" ]]; then echo "[WARN] chain-id=$CHAIN_ID (expected 1)." >&2 fi pending_nonce() { local resp hex resp=$(curl -sS -X POST "$RPC" -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\",\"params\":[\"${FROM_ADDR}\",\"pending\"],\"id\":1}" 2>/dev/null) || return 1 hex=$(echo "$resp" | jq -r '.result // empty') [[ -n "$hex" ]] || return 1 cast to-dec "$hex" } token_decimals() { cast call "$CWUSDC" 'decimals()(uint8)' --rpc-url "$BALANCE_RPC" 2>/dev/null | awk '{print $1}' } generate_spread_amounts_raw() { local count="$1" budget="$2" spread="$3" python3 - "$count" "$budget" "$spread" <<'PY' import random import sys n = int(sys.argv[1]) budget = int(sys.argv[2]) spread = float(sys.argv[3]) if n <= 0: sys.exit("count must be positive") if budget < 0: sys.exit("budget must be non-negative") if spread < 0 or spread > 100: sys.exit("spread-pct must be in [0, 100]") base = 10000 low_w = max(1, (100 * base - int(spread * base)) // 100) high_w = (100 * base + int(spread * base)) // 100 w = [random.randint(low_w, high_w) for _ in range(n)] s = sum(w) raw = [(budget * wi) // s for wi in w] rem = budget - sum(raw) for i in range(rem): raw[i % n] += 1 for x in raw: print(x) PY } stream_addresses() { if [[ -n "${LIMIT:-}" ]]; then jq -r --argjson o "$OFFSET" --argjson l "$LIMIT" '.wallets[$o:$o+$l][] | .address' "$GRID" else jq -r --argjson o "$OFFSET" '.wallets[$o:][] | .address' "$GRID" fi } ERR_LOG="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint-failures.log" LAST_IDX="${PROJECT_ROOT}/reports/status/ei-matrix-cwusdc-mint-last-idx.txt" matrix_try_mint() { local addr="$1" raw_amt="$2" idx="$3" local dec human out tx attempt=1 dec="${DECIMALS:-6}" if [[ "$raw_amt" == "0" ]]; then echo "[skip] idx=$idx $addr zero raw" return 0 fi human=$(python3 -c "d=int('$dec'); a=int('$raw_amt'); print(f'{a / (10**d):.{min(d,8)}f}')" 2>/dev/null || echo "$raw_amt") if $DRY_RUN; then if ! $QUIET_DRY_RUN; then echo "[dry-run] idx=$idx $addr raw=$raw_amt (~$human)" fi return 0 fi local cast_extra=() $CAST_LEGACY && cast_extra+=(--legacy) while [[ "$attempt" -le 2 ]]; do if out=$(cast send "$CWUSDC" "mint(address,uint256)" "$addr" "$raw_amt" \ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" \ --nonce "$NONCE" "${cast_extra[@]}" 2>&1); then tx=$(echo "$out" | tail -n1) echo "[ok] idx=$idx nonce=$NONCE $addr raw=$raw_amt (~$human) tx=$tx" sent=$((sent + 1)) NONCE=$((NONCE + 1)) echo "$idx" > "$LAST_IDX" return 0 fi if [[ "$attempt" -eq 1 ]] && echo "$out" | grep -qi 'nonce too low'; then NONCE=$(pending_nonce) || true echo "[retry] idx=$idx nonce refreshed to $NONCE (nonce too low)" >&2 attempt=$((attempt + 1)) continue fi echo "[fail] idx=$idx nonce=$NONCE $addr $out" | tee -a "$ERR_LOG" >&2 failed=$((failed + 1)) NONCE=$(pending_nonce) || true return 0 done } echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "EI matrix cWUSDC mint (mainnet)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "RPC: $RPC" echo "Token: $CWUSDC" echo "Signer: $FROM_ADDR" echo "Grid: $GRID" echo "Dry-run: $DRY_RUN Quiet: $QUIET_DRY_RUN" echo "Offset: $OFFSET Limit: ${LIMIT:-all}" if [[ -n "$MINT_RAW" ]]; then echo "Mode: fixed --mint-raw $MINT_RAW per wallet" else echo "Mode: --total-mint-raw $TOTAL_MINT_RAW spread: ±${SPREAD_PCT}% normalized" fi echo "" if [[ "${FROM_ADDR,,}" != "${DEPLOYER_CANONICAL,,}" ]]; then echo "[WARN] Signer is not canonical deployer $DEPLOYER_CANONICAL — minter role may still be granted." echo "" fi DECIMALS=$(token_decimals || echo "6") ADDR_TMP=$(mktemp) AMOUNTS_TMP=$(mktemp) cleanup_tmp() { [[ -f "$ADDR_TMP" ]] && rm -f "$ADDR_TMP" [[ -f "$AMOUNTS_TMP" ]] && rm -f "$AMOUNTS_TMP" } trap cleanup_tmp EXIT stream_addresses > "$ADDR_TMP" WALLET_COUNT=$(wc -l < "$ADDR_TMP" | tr -d '[:space:]') if [[ -z "$WALLET_COUNT" || "$WALLET_COUNT" -eq 0 ]]; then echo "No wallets in range (offset=$OFFSET limit=${LIMIT:-all})." >&2 exit 1 fi if [[ -n "$MINT_RAW" ]]; then awk -v r="$MINT_RAW" '{print r}' "$ADDR_TMP" > "$AMOUNTS_TMP" BUDGET_RAW=$((MINT_RAW * WALLET_COUNT)) else BUDGET_RAW="$TOTAL_MINT_RAW" if [[ "$BUDGET_RAW" -le 0 ]]; then echo "total-mint-raw must be positive." >&2 exit 1 fi generate_spread_amounts_raw "$WALLET_COUNT" "$BUDGET_RAW" "$SPREAD_PCT" > "$AMOUNTS_TMP" fi SUM_CHECK=$(awk '{s+=$1} END {print s}' "$AMOUNTS_TMP") if [[ "$SUM_CHECK" != "$BUDGET_RAW" ]]; then echo "INTERNAL: amount sum $SUM_CHECK != budget $BUDGET_RAW" >&2 exit 1 fi AMOUNTS_SHA256=$(sha256sum "$AMOUNTS_TMP" | awk '{print $1}') TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ") MANIFEST="$MANIFEST_DIR/ei-matrix-cwusdc-mint-manifest-${TS//:/-}.json" cat >"$MANIFEST" </dev/null | awk '{print $1}' || echo "0") ETH_HUMAN=$(python3 -c "print(f'{int(\"$ETH_WEI\") / 1e18:.6f}')" 2>/dev/null || echo "?") echo "Signer ETH (gas): ${ETH_WEI} wei (~$ETH_HUMAN ETH)" if ! $DRY_RUN && [[ "${EI_MATRIX_SKIP_GAS_CHECK:-}" != "1" ]]; then GAS_PRICE_WEI=$(cast gas-price --rpc-url "$RPC" 2>/dev/null | awk '{print $1}' | head -1) [[ -n "$GAS_PRICE_WEI" ]] || GAS_PRICE_WEI=0 MIN_WEI=$(python3 -c "c=int('$WALLET_COUNT'); g=int('$GAS_EST'); p=int('$GAS_PRICE_WEI'); b=int('$HEADROOM_BPS'); print(c*g*p*b//10000)") if python3 -c "import sys; sys.exit(0 if int('$ETH_WEI') >= int('$MIN_WEI') else 1)"; then echo "Gas preflight OK: est ${GAS_EST} gas/tx × $WALLET_COUNT × gasPrice $GAS_PRICE_WEI × (${HEADROOM_BPS}/10000) ≈ $MIN_WEI wei." else echo "Insufficient ETH for gas preflight. Need ≈ $MIN_WEI wei." >&2 echo "Set EI_MATRIX_SKIP_GAS_CHECK=1 to override (operator risk)." >&2 exit 1 fi fi echo "" echo "Sample mints:" if [[ "$WALLET_COUNT" -le 6 ]]; then _s_idx=$OFFSET while IFS=$'\t' read -r s_addr s_raw; do h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw") echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)" _s_idx=$((_s_idx + 1)) done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP") else _s_idx=$OFFSET while IFS=$'\t' read -r s_addr s_raw; do h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw") echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)" _s_idx=$((_s_idx + 1)) done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP" | head -3) _s_idx=$((OFFSET + WALLET_COUNT - 3)) while IFS=$'\t' read -r s_addr s_raw; do h=$(python3 -c "d=int('$DECIMALS'); a=int('$s_raw'); print(f'{a / (10**d):.6f}')" 2>/dev/null || echo "$s_raw") echo " idx=$_s_idx $s_addr raw=$s_raw (~$h cWUSDC)" _s_idx=$((_s_idx + 1)) done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP" | tail -3) fi echo "" sent=0 failed=0 idx=$OFFSET NONCE=$(pending_nonce) || { echo "Could not read pending nonce" >&2; exit 1; } echo "Starting nonce (pending): $NONCE" echo "" while IFS=$'\t' read -r addr raw_amt; do matrix_try_mint "$addr" "$raw_amt" "$idx" idx=$((idx + 1)) done < <(paste -d $'\t' "$ADDR_TMP" "$AMOUNTS_TMP") if $DRY_RUN; then echo "Dry-run complete. Indices covered: $OFFSET..$((idx - 1))." else echo "Done. Mint txs attempted: sent=$sent failed=$failed" fi