#!/usr/bin/env bash # Reconcile cW* token roles so only the configured per-network bridge keeps MINTER/BURNER. # # The script is intentionally conservative: # - It grants MINTER/BURNER to the configured bridge if missing # - It revokes MINTER/BURNER from the deployer/admin by default # - It can revoke MINTER/BURNER from an explicit denylist of extra addresses # - It can optionally freeze future operational role changes on newer cW* contracts # # Usage: # bash scripts/deployment/cw-enforce-bridge-only-roles.sh --dry-run # bash scripts/deployment/cw-enforce-bridge-only-roles.sh # # Optional env: # CW_ROLE_CHAINS="1 10 25 56 100 137 42161 42220 43114 8453" # CW_ROLE_TOKENS="CWUSDT CWUSDC CWEURC" # CW_ROLE_EXTRA_REVOKE="0xabc...,0xdef..." # CW_ROLE_KEEP_ALLOWLIST="0xabc...,0xdef..." # CW_ROLE_REVOKE_DEPLOYER=1 # default 1 # CW_ROLE_FREEZE_IF_SUPPORTED=1 # default 1 # CW_ROLE_FROM_BLOCK=earliest # optional log scan start block for role event discovery # PRIVATE_KEY # required unless --dry-run 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" DRY_RUN=0 if [[ "${1:-}" == "--dry-run" ]]; then DRY_RUN=1 fi 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 if ! command -v cast >/dev/null 2>&1; then echo "cast is required" >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "jq is required" >&2 exit 1 fi CHAIN_ROWS=( "1|MAINNET|ETHEREUM_MAINNET_RPC|CW_BRIDGE_MAINNET" "10|OPTIMISM|OPTIMISM_MAINNET_RPC|CW_BRIDGE_OPTIMISM" "25|CRONOS|CRONOS_MAINNET_RPC|CW_BRIDGE_CRONOS" "56|BSC|BSC_MAINNET_RPC|CW_BRIDGE_BSC" "100|GNOSIS|GNOSIS_MAINNET_RPC|CW_BRIDGE_GNOSIS" "137|POLYGON|POLYGON_MAINNET_RPC|CW_BRIDGE_POLYGON" "42161|ARBITRUM|ARBITRUM_MAINNET_RPC|CW_BRIDGE_ARBITRUM" "42220|CELO|CELO_MAINNET_RPC|CW_BRIDGE_CELO" "43114|AVALANCHE|AVALANCHE_MAINNET_RPC|CW_BRIDGE_AVALANCHE" "8453|BASE|BASE_MAINNET_RPC|CW_BRIDGE_BASE" ) TOKENS=( "CWUSDT" "CWUSDC" "CWAUSDT" "CWUSDW" "CWEURC" "CWEURT" "CWGBPC" "CWGBPT" "CWAUDC" "CWJPYC" "CWCHFC" "CWCADC" "CWXAUC" "CWXAUT" ) CHAIN_FILTER="${CW_ROLE_CHAINS:-1 10 25 56 100 137 42161 42220 43114 8453}" TOKEN_FILTER="${CW_ROLE_TOKENS:-}" REVOKE_DEPLOYER="${CW_ROLE_REVOKE_DEPLOYER:-1}" FREEZE_IF_SUPPORTED="${CW_ROLE_FREEZE_IF_SUPPORTED:-1}" EXTRA_REVOKE_RAW="${CW_ROLE_EXTRA_REVOKE:-}" KEEP_ALLOWLIST_RAW="${CW_ROLE_KEEP_ALLOWLIST:-}" ROLE_FROM_BLOCK="${CW_ROLE_FROM_BLOCK:-earliest}" MINTER_ROLE="$(cast keccak "MINTER_ROLE")" BURNER_ROLE="$(cast keccak "BURNER_ROLE")" DEFAULT_ADMIN_ROLE="0x0000000000000000000000000000000000000000000000000000000000000000" send_cmd() { if [[ "$DRY_RUN" -eq 1 ]]; then local rendered=() local redact_next=0 local arg for arg in "$@"; do if [[ "$redact_next" -eq 1 ]]; then rendered+=("") redact_next=0 continue fi if [[ "$arg" == "--private-key" ]]; then rendered+=("$arg") redact_next=1 continue fi rendered+=("$arg") done echo "[dry-run] ${rendered[*]}" return 0 fi "$@" } bool_call() { local rpc="$1" local contract="$2" local sig="$3" shift 3 cast call "$contract" "$sig" "$@" --rpc-url "$rpc" 2>/dev/null } normalize_address() { local value="${1,,}" value="${value#0x}" if [[ ${#value} -lt 40 ]]; then return 1 fi echo "0x${value: -40}" } append_unique_address() { local __var_name="$1" local candidate="${2,,}" [[ -n "$candidate" ]] || return 0 local current="${!__var_name:-}" if [[ " $current " != *" $candidate "* ]]; then printf -v "$__var_name" '%s%s ' "$current" "$candidate" fi } load_csv_addresses() { local raw="$1" local __out_var="$2" local item normalized IFS=',' read -r -a _addr_items <<< "$raw" for item in "${_addr_items[@]}"; do item="${item//[[:space:]]/}" [[ -n "$item" ]] || continue normalized="$(normalize_address "$item" || true)" [[ -n "$normalized" ]] || continue append_unique_address "$__out_var" "$normalized" done } discover_role_holders() { local rpc="$1" local token="$2" local role="$3" cast logs "RoleGranted(bytes32,address,address)" "$role" \ --from-block "$ROLE_FROM_BLOCK" --to-block latest --address "$token" --rpc-url "$rpc" --json 2>/dev/null | jq -r '.[].topics[2] // empty' | while read -r topic; do normalize_address "$topic" || true done } function_exists() { local rpc="$1" local contract="$2" local sig="$3" cast call "$contract" "$sig" --rpc-url "$rpc" >/dev/null 2>&1 } grant_role_if_supported() { local rpc="$1" local token="$2" local role="$3" local target="$4" local current current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$target" || true)" if [[ "$current" != "true" ]]; then send_cmd cast send "$token" "grantRole(bytes32,address)" "$role" "$target" \ --rpc-url "$rpc" --private-key "$PRIVATE_KEY" fi } revoke_role_if_present() { local rpc="$1" local token="$2" local role="$3" local target="$4" local current current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$target" || true)" if [[ "$current" == "true" ]]; then send_cmd cast send "$token" "revokeRole(bytes32,address)" "$role" "$target" \ --rpc-url "$rpc" --private-key "$PRIVATE_KEY" fi } role_label() { case "$1" in "$DEFAULT_ADMIN_ROLE") echo "DEFAULT_ADMIN_ROLE" ;; "$MINTER_ROLE") echo "MINTER_ROLE" ;; "$BURNER_ROLE") echo "BURNER_ROLE" ;; *) echo "$1" ;; esac } for row in "${CHAIN_ROWS[@]}"; do IFS='|' read -r chain_id chain_key rpc_var bridge_var <<< "$row" if [[ " $CHAIN_FILTER " != *" $chain_id "* ]]; then continue fi rpc="${!rpc_var:-}" bridge="${!bridge_var:-}" deployer="${DEPLOYER_ADDRESS:-${DEPLOYER_WALLET:-${DEPLOYER_EOA:-${DEFAULT_FROM_ADDRESS:-}}}}" if [[ -z "$deployer" && -n "${PRIVATE_KEY:-}" ]]; then deployer="$(cast wallet address --private-key "$PRIVATE_KEY" 2>/dev/null || true)" fi bridge="$(normalize_address "$bridge" || true)" deployer="$(normalize_address "$deployer" || true)" if [[ -z "$rpc" || -z "$bridge" ]]; then echo "Skip chain $chain_id ($chain_key): missing rpc or bridge env" continue fi echo "=== Chain $chain_id ($chain_key) ===" echo "Bridge: $bridge" for token_prefix in "${TOKENS[@]}"; do if [[ -n "$TOKEN_FILTER" && " $TOKEN_FILTER " != *" $token_prefix "* ]]; then continue fi token_var="${token_prefix}_${chain_key}" token="${!token_var:-}" if [[ -z "$token" || "$token" == "0x0000000000000000000000000000000000000000" ]]; then continue fi echo "-- $token_var $token" grant_role_if_supported "$rpc" "$token" "$MINTER_ROLE" "$bridge" grant_role_if_supported "$rpc" "$token" "$BURNER_ROLE" "$bridge" keep_allowlist="" append_unique_address keep_allowlist "$bridge" load_csv_addresses "$KEEP_ALLOWLIST_RAW" keep_allowlist revoke_candidates="" while read -r discovered; do [[ -n "$discovered" ]] || continue append_unique_address revoke_candidates "$discovered" done < <(discover_role_holders "$rpc" "$token" "$MINTER_ROLE") while read -r discovered; do [[ -n "$discovered" ]] || continue append_unique_address revoke_candidates "$discovered" done < <(discover_role_holders "$rpc" "$token" "$BURNER_ROLE") if [[ "$REVOKE_DEPLOYER" == "1" && -n "$deployer" ]]; then append_unique_address revoke_candidates "$deployer" fi load_csv_addresses "$EXTRA_REVOKE_RAW" revoke_candidates for addr in $revoke_candidates; do [[ " $keep_allowlist " == *" ${addr,,} "* ]] && continue revoke_role_if_present "$rpc" "$token" "$MINTER_ROLE" "$addr" revoke_role_if_present "$rpc" "$token" "$BURNER_ROLE" "$addr" done if [[ "$FREEZE_IF_SUPPORTED" == "1" ]]; then if function_exists "$rpc" "$token" "operationalRolesFrozen()(bool)"; then frozen="$(cast call "$token" "operationalRolesFrozen()(bool)" --rpc-url "$rpc" 2>/dev/null || true)" if [[ "$frozen" != "true" ]]; then send_cmd cast send "$token" "freezeOperationalRoles()" \ --rpc-url "$rpc" --private-key "$PRIVATE_KEY" --gas-limit 80000 fi fi fi for role in "$DEFAULT_ADMIN_ROLE" "$MINTER_ROLE" "$BURNER_ROLE"; do summary_holders="" while read -r discovered; do [[ -n "$discovered" ]] || continue current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$discovered" || true)" if [[ "$current" == "true" ]]; then append_unique_address summary_holders "$discovered" fi done < <(discover_role_holders "$rpc" "$token" "$role") echo " $(role_label "$role"): ${summary_holders:-}" done done done