Files
smom-dbis-138/scripts/deployment/complete-nonprefunded-avax-cutover.sh
defiQUG 76aa419320 feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault.
- Token-aggregation service routes, planner, chain config, relay env templates.
- Config snapshots and multi-chain deployment markdown updates.
- gitignore services/btc-intake/dist/ (tsc output); do not track dist.

Run forge build && forge test before deploy (large solc graph).

Made-with: Cursor
2026-04-07 23:40:52 -07:00

938 lines
32 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PROJECT_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
DRY_RUN=false
START_RELAYS=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--start-relays) START_RELAYS=true ;;
*)
echo "Unknown option: $arg" >&2
echo "Usage: $0 [--dry-run] [--start-relays]" >&2
exit 1
;;
esac
done
source "$SMOM_ROOT/scripts/load-env.sh" >/dev/null 2>&1
command -v cast >/dev/null 2>&1 || { echo "cast is required" >&2; exit 1; }
command -v forge >/dev/null 2>&1 || { echo "forge is required" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 1; }
DEPLOYER_ADDRESS="$(cast wallet address "$PRIVATE_KEY")"
CHAIN138_SEND_ROUTER="${CCIP_ROUTER_CHAIN138:-0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817}"
CHAIN138_RELAY_ROUTER="${CCIP_RELAY_ROUTER_CHAIN138:-}"
AVAX_SEND_ROUTER="${CCIP_AVALANCHE_ROUTER:-}"
AVAX_WETH_RELAY_ROUTER="${CCIP_RELAY_ROUTER_AVALANCHE:-0x2a0023Ad5ce1Ac6072B454575996DfFb1BB11b16}"
AVAX_RELAY_ROUTER="${CCIP_RELAY_ROUTER_AVALANCHE_CW:-${CCIP_RELAY_ROUTER_AVALANCHE:-}}"
AVAX_WETH_RELAY_BRIDGE="0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F"
CHAIN138_L1_BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
AVAX_CW_BRIDGE="${CW_BRIDGE_AVALANCHE:-}"
CW_RESERVE_VERIFIER="${CW_RESERVE_VERIFIER_CHAIN138:-}"
CW_STABLECOIN_RESERVE_VAULT_ADDR="${CW_STABLECOIN_RESERVE_VAULT:-${STABLECOIN_RESERVE_VAULT:-}}"
CW_RESERVE_SYSTEM_ADDR="${CW_RESERVE_SYSTEM:-${RESERVE_SYSTEM:-}}"
CW_CANONICAL_USDT_ADDR="${CW_CANONICAL_USDT:-${COMPLIANT_USDT_ADDRESS:-${CUSDT_ADDRESS_138:-}}}"
CW_CANONICAL_USDC_ADDR="${CW_CANONICAL_USDC:-${COMPLIANT_USDC_ADDRESS:-${CUSDC_ADDRESS_138:-}}}"
CW_USDT_RESERVE_ASSET_ADDR="${CW_USDT_RESERVE_ASSET:-${OFFICIAL_USDT_ADDRESS:-}}"
CW_USDC_RESERVE_ASSET_ADDR="${CW_USDC_RESERVE_ASSET:-${OFFICIAL_USDC_ADDRESS:-}}"
CW_MAX_OUTSTANDING_USDT_AVALANCHE="${CW_MAX_OUTSTANDING_USDT_AVALANCHE:-}"
CW_MAX_OUTSTANDING_USDC_AVALANCHE="${CW_MAX_OUTSTANDING_USDC_AVALANCHE:-}"
DRY_RUN_AVAX_SEND_ROUTER="0x0000000000000000000000000000000000004311"
DRY_RUN_AVAX_RELAY_ROUTER="0x0000000000000000000000000000000000004313"
DRY_RUN_CHAIN138_RELAY_ROUTER="0x0000000000000000000000000000000000000138"
DRY_RUN_CHAIN138_L1_BRIDGE="0x0000000000000000000000000000000000001138"
DRY_RUN_AVAX_CW_BRIDGE="0x0000000000000000000000000000000000004312"
DRY_RUN_CHAIN138_RESERVE_VERIFIER="0x0000000000000000000000000000000000001139"
LOCAL_RELAY_ROUTER_BYTECODE=""
AVAX_WETH9_ADDRESS="${WETH9_AVALANCHE:-0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08}"
ZERO_ADDRESS="0x0000000000000000000000000000000000000000"
log() {
printf '[avax-cutover] %s\n' "$*"
}
redact_secrets() {
local text="$1"
if [[ -n "${PRIVATE_KEY:-}" ]]; then
text="${text//${PRIVATE_KEY}/\$PRIVATE_KEY}"
fi
printf '%s\n' "$text"
}
format_command() {
printf '%q ' "$@"
printf '\n'
}
run_or_echo() {
local cmd="$*"
if $DRY_RUN; then
redact_secrets "$cmd"
else
eval "$cmd"
fi
}
run_command() {
if $DRY_RUN; then
redact_secrets "$(format_command "$@")"
else
"$@"
fi
}
is_truthy() {
case "${1:-}" in
1|true|TRUE|yes|YES|on|ON) return 0 ;;
*) return 1 ;;
esac
}
resolve_bool() {
local value="${1:-}"
local fallback="${2:-false}"
if [[ -z "$value" ]]; then
printf '%s\n' "$fallback"
return
fi
if is_truthy "$value"; then
printf 'true\n'
else
printf 'false\n'
fi
}
bool_to_uint() {
if [[ "${1:-false}" == "true" ]]; then
printf '1\n'
else
printf '0\n'
fi
}
DEFAULT_REQUIRE_VAULT_BACKING=false
if [[ -n "$CW_STABLECOIN_RESERVE_VAULT_ADDR" ]]; then
DEFAULT_REQUIRE_VAULT_BACKING=true
fi
DEFAULT_REQUIRE_RESERVE_SYSTEM_BALANCE=false
if [[ -n "$CW_RESERVE_SYSTEM_ADDR" ]]; then
DEFAULT_REQUIRE_RESERVE_SYSTEM_BALANCE=true
fi
DEFAULT_REQUIRE_TOKEN_OWNER_MATCH_VAULT=false
if [[ -n "$CW_STABLECOIN_RESERVE_VAULT_ADDR" ]]; then
DEFAULT_REQUIRE_TOKEN_OWNER_MATCH_VAULT=true
fi
CW_ATTACH_VERIFIER_TO_L1_BOOL="$(resolve_bool "${CW_ATTACH_VERIFIER_TO_L1:-}" "true")"
CW_REQUIRE_VAULT_BACKING_BOOL="$(resolve_bool "${CW_REQUIRE_VAULT_BACKING:-}" "$DEFAULT_REQUIRE_VAULT_BACKING")"
CW_REQUIRE_RESERVE_SYSTEM_BALANCE_BOOL="$(resolve_bool "${CW_REQUIRE_RESERVE_SYSTEM_BALANCE:-}" "$DEFAULT_REQUIRE_RESERVE_SYSTEM_BALANCE")"
CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT_BOOL="$(resolve_bool "${CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT:-}" "$DEFAULT_REQUIRE_TOKEN_OWNER_MATCH_VAULT")"
CW_FREEZE_AVAX_L2_CONFIG_BOOL="$(resolve_bool "${CW_FREEZE_AVAX_L2_CONFIG:-}" "false")"
has_code() {
local address="$1"
local rpc="$2"
[[ -n "$address" ]] || return 1
local code
code="$(cast code "$address" --rpc-url "$rpc" 2>/dev/null || true)"
[[ -n "$code" && "$code" != "0x" ]]
}
contract_call_succeeds() {
local address="$1"
local rpc="$2"
shift 2
cast call "$address" "$@" --rpc-url "$rpc" >/dev/null 2>&1
}
is_ccip_router_contract() {
local address="$1"
local rpc="$2"
has_code "$address" "$rpc" || return 1
contract_call_succeeds "$address" "$rpc" "feeToken()(address)" &&
contract_call_succeeds "$address" "$rpc" "admin()(address)" &&
contract_call_succeeds "$address" "$rpc" "supportedChains(uint64)(bool)" 138
}
is_relay_router_contract() {
local address="$1"
local rpc="$2"
has_code "$address" "$rpc" || return 1
contract_call_succeeds "$address" "$rpc" "RELAYER_ROLE()(bytes32)" &&
contract_call_succeeds "$address" "$rpc" "authorizedBridges(address)(bool)" "$AVAX_WETH_RELAY_BRIDGE"
}
get_local_relay_router_bytecode() {
if [[ -n "$LOCAL_RELAY_ROUTER_BYTECODE" ]]; then
printf '%s\n' "$LOCAL_RELAY_ROUTER_BYTECODE"
return
fi
LOCAL_RELAY_ROUTER_BYTECODE="$(
cd "$SMOM_ROOT" &&
forge inspect contracts/relay/CCIPRelayRouter.sol:CCIPRelayRouter deployedBytecode 2>/dev/null | tr '[:upper:]' '[:lower:]'
)"
printf '%s\n' "$LOCAL_RELAY_ROUTER_BYTECODE"
}
is_current_relay_router_contract() {
local address="$1"
local rpc="$2"
is_relay_router_contract "$address" "$rpc" || return 1
local local_code deployed_code
local_code="$(get_local_relay_router_bytecode)"
deployed_code="$(cast code "$address" --rpc-url "$rpc" 2>/dev/null | tr '[:upper:]' '[:lower:]')"
[[ -n "$local_code" && -n "$deployed_code" && "$local_code" == "$deployed_code" ]]
}
is_cw_l1_bridge() {
local address="$1"
local rpc="$2"
has_code "$address" "$rpc" || return 1
contract_call_succeeds "$address" "$rpc" "sendRouter()(address)" &&
contract_call_succeeds "$address" "$rpc" "receiveRouter()(address)" &&
contract_call_succeeds "$address" "$rpc" "feeToken()(address)" &&
contract_call_succeeds "$address" "$rpc" "admin()(address)" &&
contract_call_succeeds "$address" "$rpc" "supportedCanonicalToken(address)(bool)" "$COMPLIANT_USDT_ADDRESS" &&
contract_call_succeeds "$address" "$rpc" "maxOutstanding(address,uint64)(uint256)" "$COMPLIANT_USDT_ADDRESS" "$AVALANCHE_SELECTOR" &&
contract_call_succeeds "$address" "$rpc" "reserveVerifier()(address)" &&
contract_call_succeeds "$address" "$rpc" "destinations(address,uint64)((address,bool))" "$COMPLIANT_USDT_ADDRESS" "$AVALANCHE_SELECTOR" &&
! contract_call_succeeds "$address" "$rpc" "canonicalToMirrored(address)(address)" "$COMPLIANT_USDT_ADDRESS"
}
is_cw_l2_bridge() {
local address="$1"
local rpc="$2"
has_code "$address" "$rpc" || return 1
contract_call_succeeds "$address" "$rpc" "sendRouter()(address)" &&
contract_call_succeeds "$address" "$rpc" "receiveRouter()(address)" &&
contract_call_succeeds "$address" "$rpc" "feeToken()(address)" &&
contract_call_succeeds "$address" "$rpc" "admin()(address)" &&
contract_call_succeeds "$address" "$rpc" "tokenPairFrozen(address)(bool)" "$COMPLIANT_USDT_ADDRESS" &&
contract_call_succeeds "$address" "$rpc" "destinationFrozen(uint64)(bool)" 138 &&
contract_call_succeeds "$address" "$rpc" "destinations(uint64)((address,bool))" 138 &&
contract_call_succeeds "$address" "$rpc" "canonicalToMirrored(address)(address)" "$COMPLIANT_USDT_ADDRESS"
}
is_reserve_verifier_contract() {
local address="$1"
local rpc="$2"
has_code "$address" "$rpc" || return 1
contract_call_succeeds "$address" "$rpc" "bridge()(address)" &&
contract_call_succeeds "$address" "$rpc" "OPERATOR_ROLE()(bytes32)"
}
send_cast() {
local rpc="$1"
local to="$2"
local signature="$3"
shift 3
local cmd=(cast send)
local gas_price
gas_price="$(cast gas-price --rpc-url "$rpc" 2>/dev/null || true)"
if [[ "$gas_price" =~ ^[0-9]+$ && "$gas_price" -gt 0 ]]; then
gas_price=$((gas_price + gas_price / 5 + 1000000))
cmd+=(--gas-price "$gas_price")
fi
cmd+=(--rpc-url "$rpc" --private-key "$PRIVATE_KEY" --legacy "$to" "$signature")
if [[ "$#" -gt 0 ]]; then
cmd+=("$@")
fi
run_command "${cmd[@]}"
}
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
local escaped_value
escaped_value="$(printf '%s' "$value" | sed -e 's/[\\/&]/\\&/g')"
if grep -q "^${key}=" "$file"; then
sed -i "s/^${key}=.*/${key}=${escaped_value}/" "$file"
else
printf '%s=%s\n' "$key" "$value" >>"$file"
fi
}
deploy_from_broadcast() {
local script_file="$1"
local script_contract="$2"
local rpc="$3"
local chain_id="$4"
local extra_env="$5"
local broadcast_path="$SMOM_ROOT/broadcast/${script_file}/${chain_id}/run-latest.json"
if $DRY_RUN; then
redact_secrets "$extra_env forge script script/${script_file}:${script_contract} --rpc-url \"$rpc\" --broadcast --private-key \"$PRIVATE_KEY\" --legacy -vvv"
return 0
fi
(
cd "$SMOM_ROOT"
eval "$extra_env forge script script/${script_file}:${script_contract} --rpc-url \"$rpc\" --broadcast --private-key \"\$PRIVATE_KEY\" --legacy -vvv >/tmp/${script_contract}.log"
)
jq -r '.transactions[] | select(.transactionType == "CREATE") | .contractAddress' "$broadcast_path" | tail -1
}
ensure_avax_router() {
if is_ccip_router_contract "$AVAX_SEND_ROUTER" "$AVALANCHE_RPC_URL"; then
log "Using existing AVAX send router $AVAX_SEND_ROUTER"
return
fi
log "Deploying AVAX send router"
if $DRY_RUN; then
deploy_from_broadcast \
"DeployCCIPRouter.s.sol" \
"DeployCCIPRouter" \
"$AVALANCHE_RPC_URL" \
"43114" \
"CCIP_FEE_TOKEN=$LINK_TOKEN_AVALANCHE CCIP_BASE_FEE=0 CCIP_DATA_FEE_PER_BYTE=0"
AVAX_SEND_ROUTER="$DRY_RUN_AVAX_SEND_ROUTER"
return
fi
local addr
addr="$(
deploy_from_broadcast \
"DeployCCIPRouter.s.sol" \
"DeployCCIPRouter" \
"$AVALANCHE_RPC_URL" \
"43114" \
"CCIP_FEE_TOKEN=$LINK_TOKEN_AVALANCHE CCIP_BASE_FEE=0 CCIP_DATA_FEE_PER_BYTE=0"
)"
if ! $DRY_RUN; then
AVAX_SEND_ROUTER="$addr"
set_env_value "$SMOM_ROOT/.env" "CCIP_AVALANCHE_ROUTER" "$AVAX_SEND_ROUTER"
fi
}
ensure_avax_relay_router() {
if is_current_relay_router_contract "$AVAX_RELAY_ROUTER" "$AVALANCHE_RPC_URL"; then
log "Using existing AVAX cW relay router $AVAX_RELAY_ROUTER"
return
fi
log "Deploying AVAX cW relay router"
if $DRY_RUN; then
deploy_from_broadcast \
"DeployCCIPRelayRouterOnly.s.sol" \
"DeployCCIPRelayRouterOnly" \
"$AVALANCHE_RPC_URL" \
"43114" \
"RELAYER_ADDRESS=$RELAYER_ADDRESS"
AVAX_RELAY_ROUTER="$DRY_RUN_AVAX_RELAY_ROUTER"
return
fi
local addr
addr="$(
deploy_from_broadcast \
"DeployCCIPRelayRouterOnly.s.sol" \
"DeployCCIPRelayRouterOnly" \
"$AVALANCHE_RPC_URL" \
"43114" \
"RELAYER_ADDRESS=$RELAYER_ADDRESS"
)"
AVAX_RELAY_ROUTER="$addr"
set_env_value "$SMOM_ROOT/.env" "CCIP_RELAY_ROUTER_AVALANCHE_CW" "$AVAX_RELAY_ROUTER"
}
ensure_chain138_relay_router() {
if is_relay_router_contract "$CHAIN138_RELAY_ROUTER" "$RPC_URL_138"; then
log "Using existing Chain 138 relay router $CHAIN138_RELAY_ROUTER"
return
fi
log "Deploying Chain 138 relay router"
if $DRY_RUN; then
deploy_from_broadcast \
"DeployCCIPRelayRouterOnly.s.sol" \
"DeployCCIPRelayRouterOnly" \
"$RPC_URL_138" \
"138" \
"RELAYER_ADDRESS=$RELAYER_ADDRESS"
CHAIN138_RELAY_ROUTER="$DRY_RUN_CHAIN138_RELAY_ROUTER"
return
fi
local addr
addr="$(
deploy_from_broadcast \
"DeployCCIPRelayRouterOnly.s.sol" \
"DeployCCIPRelayRouterOnly" \
"$RPC_URL_138" \
"138" \
"RELAYER_ADDRESS=$RELAYER_ADDRESS"
)"
if ! $DRY_RUN; then
CHAIN138_RELAY_ROUTER="$addr"
set_env_value "$SMOM_ROOT/.env" "CCIP_RELAY_ROUTER_CHAIN138" "$CHAIN138_RELAY_ROUTER"
fi
}
ensure_l1_bridge() {
if is_cw_l1_bridge "$CHAIN138_L1_BRIDGE" "$RPC_URL_138"; then
log "Using existing Chain 138 cW L1 bridge $CHAIN138_L1_BRIDGE"
return
fi
log "Deploying Chain 138 cW L1 bridge"
if $DRY_RUN; then
deploy_from_broadcast \
"DeployCWMultiTokenBridgeL1.s.sol" \
"DeployCWMultiTokenBridgeL1" \
"$RPC_URL_138" \
"138" \
"CW_SEND_ROUTER=$CHAIN138_SEND_ROUTER CW_RECEIVE_ROUTER=$CHAIN138_RELAY_ROUTER CW_FEE_TOKEN=$LINK_TOKEN_CHAIN138"
CHAIN138_L1_BRIDGE="$DRY_RUN_CHAIN138_L1_BRIDGE"
return
fi
local addr
addr="$(
deploy_from_broadcast \
"DeployCWMultiTokenBridgeL1.s.sol" \
"DeployCWMultiTokenBridgeL1" \
"$RPC_URL_138" \
"138" \
"CW_SEND_ROUTER=$CHAIN138_SEND_ROUTER CW_RECEIVE_ROUTER=$CHAIN138_RELAY_ROUTER CW_FEE_TOKEN=$LINK_TOKEN_CHAIN138"
)"
if ! $DRY_RUN; then
CHAIN138_L1_BRIDGE="$addr"
set_env_value "$SMOM_ROOT/.env" "CW_L1_BRIDGE_CHAIN138" "$CHAIN138_L1_BRIDGE"
fi
}
ensure_l2_bridge() {
if is_cw_l2_bridge "$AVAX_CW_BRIDGE" "$AVALANCHE_RPC_URL"; then
log "Using existing AVAX cW bridge $AVAX_CW_BRIDGE"
return
fi
log "Deploying AVAX cW bridge"
if $DRY_RUN; then
deploy_from_broadcast \
"DeployCWMultiTokenBridgeL2.s.sol" \
"DeployCWMultiTokenBridgeL2" \
"$AVALANCHE_RPC_URL" \
"43114" \
"CW_SEND_ROUTER=$AVAX_SEND_ROUTER CW_RECEIVE_ROUTER=$AVAX_RELAY_ROUTER CW_FEE_TOKEN=$LINK_TOKEN_AVALANCHE"
AVAX_CW_BRIDGE="$DRY_RUN_AVAX_CW_BRIDGE"
return
fi
local addr
addr="$(
deploy_from_broadcast \
"DeployCWMultiTokenBridgeL2.s.sol" \
"DeployCWMultiTokenBridgeL2" \
"$AVALANCHE_RPC_URL" \
"43114" \
"CW_SEND_ROUTER=$AVAX_SEND_ROUTER CW_RECEIVE_ROUTER=$AVAX_RELAY_ROUTER CW_FEE_TOKEN=$LINK_TOKEN_AVALANCHE"
)"
if ! $DRY_RUN; then
AVAX_CW_BRIDGE="$addr"
set_env_value "$SMOM_ROOT/.env" "CW_BRIDGE_AVALANCHE" "$AVAX_CW_BRIDGE"
fi
}
ensure_l2_receive_router() {
local current
current="$(cast call "$AVAX_CW_BRIDGE" "receiveRouter()(address)" --rpc-url "$AVALANCHE_RPC_URL" 2>/dev/null || true)"
if [[ "${current,,}" == "${AVAX_RELAY_ROUTER,,}" ]]; then
log "AVAX cW bridge already points at relay router $AVAX_RELAY_ROUTER"
return
fi
send_cast "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "setReceiveRouter(address)" "$AVAX_RELAY_ROUTER"
}
ensure_supported_chain() {
local rpc="$1"
local router="$2"
local selector="$3"
local label="$4"
local current
current="$(cast call "$router" "supportedChains(uint64)(bool)" "$selector" --rpc-url "$rpc" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "$label already supports selector $selector"
return
fi
send_cast "$rpc" "$router" "addSupportedChain(uint64)" "$selector"
}
ensure_authorized_bridge() {
local rpc="$1"
local router="$2"
local bridge="$3"
local current
current="$(cast call "$router" "authorizedBridges(address)(bool)" "$bridge" --rpc-url "$rpc" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "Bridge $bridge already authorized on $router"
return
fi
send_cast "$rpc" "$router" "authorizeBridge(address)" "$bridge"
}
ensure_relayer_role() {
local rpc="$1"
local router="$2"
local role
role="$(cast keccak "RELAYER_ROLE")"
local current
current="$(cast call "$router" "hasRole(bytes32,address)(bool)" "$role" "$RELAYER_ADDRESS" --rpc-url "$rpc" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "Relayer role already granted on $router"
return
fi
send_cast "$rpc" "$router" "grantRelayerRole(address)" "$RELAYER_ADDRESS"
}
ensure_l2_token_pair() {
local canonical="$1"
local mirrored="$2"
local current
current="$(cast call "$AVAX_CW_BRIDGE" "canonicalToMirrored(address)(address)" "$canonical" --rpc-url "$AVALANCHE_RPC_URL" 2>/dev/null || echo "0x0")"
if [[ "${current,,}" == "${mirrored,,}" ]]; then
log "Token pair $canonical -> $mirrored already configured"
return
fi
send_cast "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "configureTokenPair(address,address)" "$canonical" "$mirrored"
}
ensure_destination_l1() {
local token="$1"
local encoded
encoded="$(cast call "$CHAIN138_L1_BRIDGE" "destinations(address,uint64)((address,bool))" "$token" "$AVALANCHE_SELECTOR" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
encoded="${encoded,,}"
if [[ "$encoded" == *"${AVAX_CW_BRIDGE,,}"* && "$encoded" == *"true"* ]]; then
log "L1 destination already configured for $token"
return
fi
send_cast "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "configureDestination(address,uint64,address,bool)" "$token" "$AVALANCHE_SELECTOR" "$AVAX_CW_BRIDGE" true
}
ensure_destination_l2() {
local encoded
encoded="$(cast call "$AVAX_CW_BRIDGE" "destinations(uint64)((address,bool))" 138 --rpc-url "$AVALANCHE_RPC_URL" 2>/dev/null || true)"
encoded="${encoded,,}"
if [[ "$encoded" == *"${CHAIN138_L1_BRIDGE,,}"* && "$encoded" == *"true"* ]]; then
log "L2 destination back to Chain 138 already configured"
return
fi
send_cast "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "configureDestination(uint64,address,bool)" 138 "$CHAIN138_L1_BRIDGE" true
}
ensure_supported_canonical_token() {
local token="$1"
local label="$2"
local current
current="$(cast call "$CHAIN138_L1_BRIDGE" "supportedCanonicalToken(address)(bool)" "$token" --rpc-url "$RPC_URL_138" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "$label already allowlisted on Chain 138 cW bridge"
return
fi
send_cast "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "configureSupportedCanonicalToken(address,bool)" "$token" true
}
ensure_max_outstanding() {
local token="$1"
local amount="$2"
local label="$3"
if [[ -z "$amount" ]]; then
log "Skipping maxOutstanding for $label: CW_MAX_OUTSTANDING not set"
return
fi
local current
current="$(cast call "$CHAIN138_L1_BRIDGE" "maxOutstanding(address,uint64)(uint256)" "$token" "$AVALANCHE_SELECTOR" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
if [[ "$current" == "$amount" ]]; then
log "maxOutstanding already set for $label: $amount"
return
fi
send_cast "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "setMaxOutstanding(address,uint64,uint256)" "$token" "$AVALANCHE_SELECTOR" "$amount"
}
build_verifier_deploy_env() {
local -a env_parts=()
env_parts+=("CW_L1_BRIDGE=$CHAIN138_L1_BRIDGE")
env_parts+=("CW_ATTACH_VERIFIER_TO_L1=$(bool_to_uint "$CW_ATTACH_VERIFIER_TO_L1_BOOL")")
env_parts+=("CW_CANONICAL_USDT=$CW_CANONICAL_USDT_ADDR")
env_parts+=("CW_CANONICAL_USDC=$CW_CANONICAL_USDC_ADDR")
env_parts+=("CW_USDT_RESERVE_ASSET=${CW_USDT_RESERVE_ASSET_ADDR:-$ZERO_ADDRESS}")
env_parts+=("CW_USDC_RESERVE_ASSET=${CW_USDC_RESERVE_ASSET_ADDR:-$ZERO_ADDRESS}")
env_parts+=("CW_REQUIRE_VAULT_BACKING=$(bool_to_uint "$CW_REQUIRE_VAULT_BACKING_BOOL")")
env_parts+=("CW_REQUIRE_RESERVE_SYSTEM_BALANCE=$(bool_to_uint "$CW_REQUIRE_RESERVE_SYSTEM_BALANCE_BOOL")")
env_parts+=("CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT=$(bool_to_uint "$CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT_BOOL")")
if [[ -n "$CW_STABLECOIN_RESERVE_VAULT_ADDR" ]]; then
env_parts+=("CW_STABLECOIN_RESERVE_VAULT=$CW_STABLECOIN_RESERVE_VAULT_ADDR")
fi
if [[ -n "$CW_RESERVE_SYSTEM_ADDR" ]]; then
env_parts+=("CW_RESERVE_SYSTEM=$CW_RESERVE_SYSTEM_ADDR")
fi
printf '%s ' "${env_parts[@]}"
}
ensure_verifier_bridge_binding() {
local current
current="$(cast call "$CW_RESERVE_VERIFIER" "bridge()(address)" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
if [[ "${current,,}" == "${CHAIN138_L1_BRIDGE,,}" ]]; then
return
fi
send_cast "$RPC_URL_138" "$CW_RESERVE_VERIFIER" "setBridge(address)" "$CHAIN138_L1_BRIDGE"
}
ensure_verifier_stablecoin_reserve_vault() {
[[ -n "$CW_STABLECOIN_RESERVE_VAULT_ADDR" ]] || return
local current
current="$(cast call "$CW_RESERVE_VERIFIER" "stablecoinReserveVault()(address)" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
if [[ "${current,,}" == "${CW_STABLECOIN_RESERVE_VAULT_ADDR,,}" ]]; then
return
fi
send_cast "$RPC_URL_138" "$CW_RESERVE_VERIFIER" "setStablecoinReserveVault(address)" "$CW_STABLECOIN_RESERVE_VAULT_ADDR"
}
ensure_verifier_reserve_system() {
[[ -n "$CW_RESERVE_SYSTEM_ADDR" ]] || return
local current
current="$(cast call "$CW_RESERVE_VERIFIER" "reserveSystem()(address)" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
if [[ "${current,,}" == "${CW_RESERVE_SYSTEM_ADDR,,}" ]]; then
return
fi
send_cast "$RPC_URL_138" "$CW_RESERVE_VERIFIER" "setReserveSystem(address)" "$CW_RESERVE_SYSTEM_ADDR"
}
ensure_verifier_token_config() {
local canonical="$1"
local reserve_asset="${2:-$ZERO_ADDRESS}"
local label="$3"
[[ -n "$canonical" ]] || {
log "Skipping verifier config for $label: canonical token missing"
return
}
if [[ "$CW_REQUIRE_RESERVE_SYSTEM_BALANCE_BOOL" == "true" && "$reserve_asset" == "$ZERO_ADDRESS" ]]; then
log "Skipping verifier config for $label: reserve asset missing while reserve-system balance checks are enabled"
return
fi
local current normalized
current="$(cast call "$CW_RESERVE_VERIFIER" "tokenConfigs(address)(bool,address,bool,bool,bool)" "$canonical" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
normalized="$(printf '%s' "$current" | tr '\n' ' ' | xargs 2>/dev/null || true)"
local enabled current_reserve current_require_vault current_require_reserve current_require_owner
read -r enabled current_reserve current_require_vault current_require_reserve current_require_owner <<<"$normalized"
if [[ "$enabled" == "true" &&
"${current_reserve,,}" == "${reserve_asset,,}" &&
"$current_require_vault" == "$CW_REQUIRE_VAULT_BACKING_BOOL" &&
"$current_require_reserve" == "$CW_REQUIRE_RESERVE_SYSTEM_BALANCE_BOOL" &&
"$current_require_owner" == "$CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT_BOOL" ]]; then
log "Reserve verifier already configured for $label"
return
fi
send_cast \
"$RPC_URL_138" \
"$CW_RESERVE_VERIFIER" \
"configureToken(address,address,bool,bool,bool)" \
"$canonical" \
"$reserve_asset" \
"$CW_REQUIRE_VAULT_BACKING_BOOL" \
"$CW_REQUIRE_RESERVE_SYSTEM_BALANCE_BOOL" \
"$CW_REQUIRE_TOKEN_OWNER_MATCH_VAULT_BOOL"
}
ensure_reserve_verifier() {
local current
current="$(cast call "$CHAIN138_L1_BRIDGE" "reserveVerifier()(address)" --rpc-url "$RPC_URL_138" 2>/dev/null || true)"
if is_reserve_verifier_contract "$current" "$RPC_URL_138"; then
CW_RESERVE_VERIFIER="$current"
log "Using existing Chain 138 reserve verifier $CW_RESERVE_VERIFIER"
elif is_reserve_verifier_contract "$CW_RESERVE_VERIFIER" "$RPC_URL_138"; then
log "Using configured Chain 138 reserve verifier $CW_RESERVE_VERIFIER"
else
log "Deploying Chain 138 reserve verifier"
if $DRY_RUN; then
deploy_from_broadcast \
"DeployCWReserveVerifier.s.sol" \
"DeployCWReserveVerifier" \
"$RPC_URL_138" \
"138" \
"$(build_verifier_deploy_env)"
CW_RESERVE_VERIFIER="$DRY_RUN_CHAIN138_RESERVE_VERIFIER"
return
fi
local addr
addr="$(
deploy_from_broadcast \
"DeployCWReserveVerifier.s.sol" \
"DeployCWReserveVerifier" \
"$RPC_URL_138" \
"138" \
"$(build_verifier_deploy_env)"
)"
CW_RESERVE_VERIFIER="$addr"
fi
if ! $DRY_RUN; then
set_env_value "$SMOM_ROOT/.env" "CW_RESERVE_VERIFIER_CHAIN138" "$CW_RESERVE_VERIFIER"
fi
if [[ "$CW_ATTACH_VERIFIER_TO_L1_BOOL" == "true" && "${current,,}" != "${CW_RESERVE_VERIFIER,,}" ]]; then
send_cast "$RPC_URL_138" "$CHAIN138_L1_BRIDGE" "setReserveVerifier(address)" "$CW_RESERVE_VERIFIER"
fi
ensure_verifier_bridge_binding
ensure_verifier_stablecoin_reserve_vault
ensure_verifier_reserve_system
ensure_verifier_token_config "$CW_CANONICAL_USDT_ADDR" "${CW_USDT_RESERVE_ASSET_ADDR:-$ZERO_ADDRESS}" "cUSDT"
ensure_verifier_token_config "$CW_CANONICAL_USDC_ADDR" "${CW_USDC_RESERVE_ASSET_ADDR:-$ZERO_ADDRESS}" "cUSDC"
}
ensure_l2_token_pair_frozen() {
local canonical="$1"
local label="$2"
local current
current="$(cast call "$AVAX_CW_BRIDGE" "tokenPairFrozen(address)(bool)" "$canonical" --rpc-url "$AVALANCHE_RPC_URL" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "$label token pair already frozen on Avalanche"
return
fi
send_cast "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "freezeTokenPair(address)" "$canonical"
}
ensure_l2_destination_frozen() {
local current
current="$(cast call "$AVAX_CW_BRIDGE" "destinationFrozen(uint64)(bool)" 138 --rpc-url "$AVALANCHE_RPC_URL" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "Avalanche destination back to Chain 138 already frozen"
return
fi
send_cast "$AVALANCHE_RPC_URL" "$AVAX_CW_BRIDGE" "freezeDestination(uint64)" 138
}
ensure_role() {
local rpc="$1"
local token="$2"
local role_name="$3"
local grantee="$4"
local role
role="$(cast keccak "$role_name")"
local current
current="$(cast call "$token" "hasRole(bytes32,address)(bool)" "$role" "$grantee" --rpc-url "$rpc" 2>/dev/null || echo "false")"
if [[ "$current" == "true" ]]; then
log "$role_name already granted on $token"
return
fi
send_cast "$rpc" "$token" "grantRole(bytes32,address)" "$role" "$grantee"
}
write_relay_profiles() {
local avax_profile="$SMOM_ROOT/services/relay/.env.avax"
local avax_cw_profile="$SMOM_ROOT/services/relay/.env.avax-cw"
local reverse_profile="$SMOM_ROOT/services/relay/.env.avax-to-138"
if $DRY_RUN; then
cat <<EOF
# Would update $avax_profile:
DEST_RELAY_ROUTER=${AVAX_WETH_RELAY_ROUTER}
DEST_RELAY_BRIDGE_ALLOWLIST=${AVAX_WETH_RELAY_BRIDGE}
# Would write $avax_cw_profile:
RPC_URL_138=${RPC_URL_138}
CCIP_ROUTER_CHAIN138=${CHAIN138_SEND_ROUTER}
CCIPWETH9_BRIDGE_CHAIN138=${CHAIN138_L1_BRIDGE}
SOURCE_CHAIN_SELECTOR=138
DEST_CHAIN_NAME=Avalanche
DEST_CHAIN_ID=43114
DEST_RPC_URL=${AVALANCHE_RPC_URL}
DEST_CHAIN_SELECTOR=${AVALANCHE_SELECTOR}
DEST_RELAY_ROUTER=${AVAX_RELAY_ROUTER}
DEST_RELAY_BRIDGE=${AVAX_CW_BRIDGE}
DEST_WETH9_ADDRESS=${AVAX_WETH9_ADDRESS}
RELAYER_PRIVATE_KEY=\${PRIVATE_KEY}
RELAYER_ADDRESS=${RELAYER_ADDRESS}
START_BLOCK=latest
POLL_INTERVAL=5000
CONFIRMATION_BLOCKS=1
MAX_RETRIES=3
RETRY_DELAY=5000
LOG_LEVEL=info
DEST_RELAY_BRIDGE_ALLOWLIST=${AVAX_CW_BRIDGE}
# Would write $reverse_profile:
SOURCE_CHAIN_NAME=Avalanche
SOURCE_CHAIN_ID=43114
SOURCE_CHAIN_SELECTOR=${AVALANCHE_SELECTOR}
SOURCE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
SOURCE_ROUTER_ADDRESS=${AVAX_SEND_ROUTER}
DEST_CHAIN_NAME=Chain 138
DEST_CHAIN_ID=138
DEST_CHAIN_SELECTOR=138
DEST_RPC_URL=${RPC_URL_138}
DEST_RELAY_ROUTER=${CHAIN138_RELAY_ROUTER}
DEST_RELAY_BRIDGE=${CHAIN138_L1_BRIDGE}
DEST_RELAY_BRIDGE_ALLOWLIST=${CHAIN138_L1_BRIDGE}
RELAYER_PRIVATE_KEY=\${PRIVATE_KEY}
RELAYER_ADDRESS=${RELAYER_ADDRESS}
RELAY_DEST_LEGACY_TX=1
RELAY_DEST_GAS_PRICE_BUMP_PCT=20
RELAY_DEST_GAS_PRICE_BUMP_WEI=1000000
EOF
return
fi
set_env_value "$avax_profile" "DEST_RELAY_ROUTER" "$AVAX_WETH_RELAY_ROUTER"
set_env_value "$avax_profile" "DEST_RELAY_BRIDGE_ALLOWLIST" "$AVAX_WETH_RELAY_BRIDGE"
cat >"$avax_cw_profile" <<EOF
# Forward relay profile for non-prefunded AVAX cW minting.
RPC_URL_138=${RPC_URL_138}
CCIP_ROUTER_CHAIN138=${CHAIN138_SEND_ROUTER}
CCIPWETH9_BRIDGE_CHAIN138=${CHAIN138_L1_BRIDGE}
SOURCE_CHAIN_SELECTOR=138
DEST_CHAIN_NAME=Avalanche
DEST_CHAIN_ID=43114
DEST_RPC_URL=${AVALANCHE_RPC_URL}
DEST_CHAIN_SELECTOR=${AVALANCHE_SELECTOR}
DEST_RELAY_ROUTER=${AVAX_RELAY_ROUTER}
DEST_RELAY_BRIDGE=${AVAX_CW_BRIDGE}
DEST_WETH9_ADDRESS=${AVAX_WETH9_ADDRESS}
RELAYER_PRIVATE_KEY=\${PRIVATE_KEY}
RELAYER_ADDRESS=${RELAYER_ADDRESS}
START_BLOCK=latest
POLL_INTERVAL=5000
CONFIRMATION_BLOCKS=1
MAX_RETRIES=3
RETRY_DELAY=5000
LOG_LEVEL=info
DEST_RELAY_BRIDGE_ALLOWLIST=${AVAX_CW_BRIDGE}
EOF
cat >"$reverse_profile" <<EOF
# Reverse relay profile for AVAX cW burns back to Chain 138.
SOURCE_CHAIN_NAME=Avalanche
SOURCE_CHAIN_ID=43114
SOURCE_CHAIN_SELECTOR=${AVALANCHE_SELECTOR}
SOURCE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
SOURCE_ROUTER_ADDRESS=${AVAX_SEND_ROUTER}
DEST_CHAIN_NAME=Chain 138
DEST_CHAIN_ID=138
DEST_CHAIN_SELECTOR=138
DEST_RPC_URL=${RPC_URL_138}
DEST_RELAY_ROUTER=${CHAIN138_RELAY_ROUTER}
DEST_RELAY_BRIDGE=${CHAIN138_L1_BRIDGE}
DEST_RELAY_BRIDGE_ALLOWLIST=${CHAIN138_L1_BRIDGE}
RELAYER_PRIVATE_KEY=\${PRIVATE_KEY}
RELAYER_ADDRESS=${RELAYER_ADDRESS}
START_BLOCK=latest
POLL_INTERVAL=5000
CONFIRMATION_BLOCKS=1
MAX_RETRIES=3
RETRY_DELAY=5000
LOG_LEVEL=info
RELAY_DEST_LEGACY_TX=1
RELAY_DEST_GAS_PRICE_BUMP_PCT=20
RELAY_DEST_GAS_PRICE_BUMP_WEI=1000000
EOF
}
start_relays() {
if ! $START_RELAYS; then
return
fi
if $DRY_RUN; then
echo "(cd \"$SMOM_ROOT/services/relay\" && RELAY_SKIP_ENV_LOCAL=1 ./start-relay.sh avax)"
echo "(cd \"$SMOM_ROOT/services/relay\" && RELAY_SKIP_ENV_LOCAL=1 ./start-relay.sh avax-cw)"
echo "(cd \"$SMOM_ROOT/services/relay\" && RELAY_SKIP_ENV_LOCAL=1 ./start-relay.sh avax-to-138)"
return
fi
(cd "$SMOM_ROOT/services/relay" && nohup env RELAY_SKIP_ENV_LOCAL=1 ./start-relay.sh avax >/tmp/relay-avax.log 2>&1 &)
(cd "$SMOM_ROOT/services/relay" && nohup env RELAY_SKIP_ENV_LOCAL=1 ./start-relay.sh avax-cw >/tmp/relay-avax-cw.log 2>&1 &)
(cd "$SMOM_ROOT/services/relay" && nohup env RELAY_SKIP_ENV_LOCAL=1 ./start-relay.sh avax-to-138 >/tmp/relay-avax-to-138.log 2>&1 &)
}
log "Deployer: $DEPLOYER_ADDRESS"
log "Dry run: $DRY_RUN"
ensure_avax_router
ensure_avax_relay_router
ensure_chain138_relay_router
ensure_l1_bridge
ensure_l2_bridge
if ! $DRY_RUN; then
ensure_supported_chain "$AVALANCHE_RPC_URL" "$AVAX_SEND_ROUTER" 138 "AVAX router"
fi
ensure_l2_receive_router
ensure_authorized_bridge "$AVALANCHE_RPC_URL" "$AVAX_RELAY_ROUTER" "$AVAX_CW_BRIDGE"
ensure_relayer_role "$AVALANCHE_RPC_URL" "$AVAX_RELAY_ROUTER"
ensure_authorized_bridge "$RPC_URL_138" "$CHAIN138_RELAY_ROUTER" "$CHAIN138_L1_BRIDGE"
ensure_relayer_role "$RPC_URL_138" "$CHAIN138_RELAY_ROUTER"
ensure_l2_token_pair "$COMPLIANT_USDT_ADDRESS" "$CWUSDT_AVALANCHE"
ensure_l2_token_pair "$COMPLIANT_USDC_ADDRESS" "$CWUSDC_AVALANCHE"
ensure_role "$AVALANCHE_RPC_URL" "$CWUSDT_AVALANCHE" "MINTER_ROLE" "$AVAX_CW_BRIDGE"
ensure_role "$AVALANCHE_RPC_URL" "$CWUSDT_AVALANCHE" "BURNER_ROLE" "$AVAX_CW_BRIDGE"
ensure_role "$AVALANCHE_RPC_URL" "$CWUSDC_AVALANCHE" "MINTER_ROLE" "$AVAX_CW_BRIDGE"
ensure_role "$AVALANCHE_RPC_URL" "$CWUSDC_AVALANCHE" "BURNER_ROLE" "$AVAX_CW_BRIDGE"
ensure_supported_canonical_token "$CW_CANONICAL_USDT_ADDR" "cUSDT"
ensure_supported_canonical_token "$CW_CANONICAL_USDC_ADDR" "cUSDC"
ensure_max_outstanding "$CW_CANONICAL_USDT_ADDR" "$CW_MAX_OUTSTANDING_USDT_AVALANCHE" "cUSDT -> Avalanche"
ensure_max_outstanding "$CW_CANONICAL_USDC_ADDR" "$CW_MAX_OUTSTANDING_USDC_AVALANCHE" "cUSDC -> Avalanche"
ensure_destination_l1 "$COMPLIANT_USDT_ADDRESS"
ensure_destination_l1 "$COMPLIANT_USDC_ADDRESS"
ensure_destination_l2
ensure_reserve_verifier
if [[ "$CW_FREEZE_AVAX_L2_CONFIG_BOOL" == "true" ]]; then
ensure_l2_token_pair_frozen "$CW_CANONICAL_USDT_ADDR" "cUSDT"
ensure_l2_token_pair_frozen "$CW_CANONICAL_USDC_ADDR" "cUSDC"
ensure_l2_destination_frozen
else
log "Skipping Avalanche L2 freeze; set CW_FREEZE_AVAX_L2_CONFIG=1 to lock token pairs and destination"
fi
write_relay_profiles
start_relays
log "Summary:"
log " AVAX send router: ${AVAX_SEND_ROUTER:-<dry-run>}"
log " AVAX cW relay router: ${AVAX_RELAY_ROUTER:-<dry-run>}"
log " Chain 138 relay router:${CHAIN138_RELAY_ROUTER:-<dry-run>}"
log " Chain 138 L1 bridge: ${CHAIN138_L1_BRIDGE:-<dry-run>}"
log " AVAX cW bridge: ${AVAX_CW_BRIDGE:-<dry-run>}"
log " Chain 138 verifier: ${CW_RESERVE_VERIFIER:-<not-set>}"
log " Freeze AVAX L2 config: ${CW_FREEZE_AVAX_L2_CONFIG_BOOL}"