#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" cd "$SMOM_ROOT" if [[ -f "$SMOM_ROOT/scripts/lib/deployment/dotenv.sh" ]]; then # shellcheck disable=SC1091 source "$SMOM_ROOT/scripts/lib/deployment/dotenv.sh" load_deployment_env --repo-root "$SMOM_ROOT" else source "$SMOM_ROOT/scripts/load-env.sh" >/dev/null 2>&1 || true fi PLAN_JSON="${PLAN_JSON:-$SMOM_ROOT/config/chain138-eth-pmm-liquidity-plan.json}" PROFILE="${PROFILE:-}" EXECUTE="${EXECUTE:-0}" RPC_URL_138="${RPC_URL_138:-${RPC_URL:-http://192.168.11.211:8545}}" DODO_PMM_INTEGRATION_ADDRESS="${DODO_PMM_INTEGRATION_ADDRESS:-${DODO_PMM_INTEGRATION:-${CHAIN_138_DODO_PMM_INTEGRATION:-0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d}}}" PRIVATE_KEY="${PRIVATE_KEY:-}" command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 1; } command -v cast >/dev/null 2>&1 || { echo "cast is required" >&2; exit 1; } [[ -f "$PLAN_JSON" ]] || { echo "PLAN_JSON not found: $PLAN_JSON" >&2; exit 1; } [[ -n "$DODO_PMM_INTEGRATION_ADDRESS" ]] || { echo "DODO_PMM_INTEGRATION_ADDRESS not set" >&2; exit 1; } require_private_key_env || exit 1 EXECUTION_CONFIG="$(jq -r '.executionConfig' "$PLAN_JSON")" if [[ "$EXECUTION_CONFIG" != /* ]]; then EXECUTION_CONFIG="$SMOM_ROOT/${EXECUTION_CONFIG#smom-dbis-138/}" fi [[ -f "$EXECUTION_CONFIG" ]] || { echo "Execution config not found: $EXECUTION_CONFIG" >&2; exit 1; } if [[ -z "$PROFILE" ]]; then PROFILE="$(jq -r '.defaultProfile' "$PLAN_JSON")" fi FIAT_BASE_UNITS="$(jq -r --arg p "$PROFILE" '.profiles[$p].fiatBaseUnits' "$PLAN_JSON")" XAU_BASE_UNITS="$(jq -r --arg p "$PROFILE" '.profiles[$p].xauBaseUnits' "$PLAN_JSON")" [[ "$FIAT_BASE_UNITS" != "null" ]] || { echo "Unknown PROFILE: $PROFILE" >&2; exit 1; } [[ "$XAU_BASE_UNITS" != "null" ]] || { echo "Unknown PROFILE: $PROFILE" >&2; exit 1; } APPROVE_GAS_LIMIT="$(jq -r '.execution.approveGasLimit' "$PLAN_JSON")" MINT_GAS_LIMIT="$(jq -r '.execution.mintGasLimit' "$PLAN_JSON")" WRAP_GAS_LIMIT="$(jq -r '.execution.wrapGasLimit' "$PLAN_JSON")" ADD_LIQUIDITY_GAS_LIMIT="$(jq -r '.execution.addLiquidityGasLimit' "$PLAN_JSON")" GAS_PRICE_WEI="${CHAIN_GAS_PRICE:-$(jq -r '.execution.legacyGasPriceWei' "$PLAN_JSON")}" WRAP_WHEN_NEEDED="$(jq -r '.execution.wrapNativeEthWhenNeeded' "$PLAN_JSON")" MINT_WHEN_OWNER="$(jq -r '.execution.mintMissingBaseWhenOwner' "$PLAN_JSON")" APPROVE_MAX="$(jq -r '.execution.approveMax' "$PLAN_JSON")" DEPLOYER="$(derive_deployer_address || true)" [[ -n "$DEPLOYER" ]] || { echo "ERROR: Could not derive DEPLOYER_ADDRESS from PRIVATE_KEY." >&2; exit 1; } WETH_ADDRESS="$(jq -r '.tokens.WETH' "$EXECUTION_CONFIG")" MAX_UINT="0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ZERO_ADDR="0x0000000000000000000000000000000000000000" log() { printf '%s\n' "$*" } get_pool_reserves() { local pool="$1" local lines mapfile -t lines < <(cast call "$pool" 'getVaultReserve()(uint256,uint256)' --rpc-url "$RPC_URL_138") printf '%s\t%s\n' "$(uint_clean "${lines[0]:-0}")" "$(uint_clean "${lines[1]:-0}")" } uint_clean() { printf '%s\n' "$1" | awk '{print $1}' } hex_to_addr() { local raw="${1#0x}" if [[ ${#raw} -lt 40 ]]; then printf '%s\n' "$ZERO_ADDR" return 0 fi printf '0x%s\n' "${raw: -40}" } send_tx() { local to="$1" shift cast send "$to" "$@" \ --rpc-url "$RPC_URL_138" \ --private-key "$PRIVATE_KEY" \ --legacy \ --gas-price "$GAS_PRICE_WEI" \ -q } big_add() { python3 - "$1" "$2" <<'PY' import sys print(int(sys.argv[1]) + int(sys.argv[2])) PY } big_sub_if_positive() { python3 - "$1" "$2" <<'PY' import sys a = int(sys.argv[1]) b = int(sys.argv[2]) print(a - b if a > b else 0) PY } big_lt() { python3 - "$1" "$2" <<'PY' import sys print("1" if int(sys.argv[1]) < int(sys.argv[2]) else "0") PY } raw_to_human_6() { awk -v v="$1" 'BEGIN { printf "%.6f", v / 1000000 }' } raw_to_human_18() { awk -v v="$1" 'BEGIN { printf "%.18f", v / 1000000000000000000 }' } mul_div_round() { awk -v a="$1" -v b="$2" -v c="$3" 'BEGIN { printf "%.0f", (a * b) / c }' } derive_usd_per_eth() { local pool base_reserve quote_reserve local cusdt quote cusdt="$(jq -r '.tokens.cUSDT' "$EXECUTION_CONFIG")" quote="$WETH_ADDRESS" pool="$(hex_to_addr "$(cast call "$DODO_PMM_INTEGRATION_ADDRESS" 'pools(address,address)(address)' "$cusdt" "$quote" --rpc-url "$RPC_URL_138")")" [[ "$pool" != "$ZERO_ADDR" ]] || { echo "0"; return 1; } read -r base_reserve quote_reserve < <(get_pool_reserves "$pool") if [[ "$base_reserve" == "0" || "$quote_reserve" == "0" ]]; then echo "0" return 1 fi awk -v base="$base_reserve" -v quote="$quote_reserve" 'BEGIN { printf "%.12f", (base / 1000000) / (quote / 1000000000000000000) }' } derive_xau_usd_per_unit() { local pool base_symbol quote_symbol base_addr quote_addr base_reserve quote_reserve fallback base_symbol="$(jq -r '.xauPricing.poolBaseSymbol' "$PLAN_JSON")" quote_symbol="$(jq -r '.xauPricing.poolQuoteSymbol' "$PLAN_JSON")" fallback="$(jq -r '.xauPricing.fallbackUsdPerUnit' "$PLAN_JSON")" base_addr="$(jq -r --arg sym "$base_symbol" '.tokens[$sym]' "$EXECUTION_CONFIG")" quote_addr="$(jq -r --arg sym "$quote_symbol" '.tokens[$sym]' "$EXECUTION_CONFIG")" pool="$(hex_to_addr "$(cast call "$DODO_PMM_INTEGRATION_ADDRESS" 'pools(address,address)(address)' "$base_addr" "$quote_addr" --rpc-url "$RPC_URL_138")")" if [[ "$pool" == "$ZERO_ADDR" ]]; then echo "$fallback" return 0 fi read -r base_reserve quote_reserve < <(get_pool_reserves "$pool") if [[ "$base_reserve" == "0" || "$quote_reserve" == "0" ]]; then echo "$fallback" return 0 fi awk -v base="$base_reserve" -v quote="$quote_reserve" 'BEGIN { printf "%.12f", (quote / 1000000) / (base / 1000000) }' } usd_per_eth="$(derive_usd_per_eth)" [[ "$usd_per_eth" != "0" ]] || { echo "Could not derive USD per ETH from live cUSDT/WETH pool" >&2; exit 1; } xau_usd_per_unit="$(derive_xau_usd_per_unit)" log "=== Chain 138 ETH PMM Liquidity Funding ===" log "Mode: $( [[ "$EXECUTE" == "1" ]] && echo EXECUTE || echo DRY_RUN )" log "Profile: $PROFILE" log "Plan: $PLAN_JSON" log "Config: $EXECUTION_CONFIG" log "Integration: $DODO_PMM_INTEGRATION_ADDRESS" log "RPC: $RPC_URL_138" log "Deployer: $DEPLOYER" log "USD per ETH: $usd_per_eth" log "XAU USD: $xau_usd_per_unit" log "" total_quote_units=0 wrap_needed=0 declare -a ROWS=() while IFS=$'\t' read -r base_symbol base_address; do pool="$(hex_to_addr "$(cast call "$DODO_PMM_INTEGRATION_ADDRESS" 'pools(address,address)(address)' "$base_address" "$WETH_ADDRESS" --rpc-url "$RPC_URL_138")")" [[ "$pool" != "$ZERO_ADDR" ]] || continue case "$base_symbol" in cXAUC|cXAUT) base_units="$XAU_BASE_UNITS" usd_per_unit="$xau_usd_per_unit" ;; *) base_units="$FIAT_BASE_UNITS" usd_per_unit="$(jq -r --arg sym "$base_symbol" '.usdPerUnit[$sym]' "$PLAN_JSON")" [[ "$usd_per_unit" != "null" ]] || { echo "Missing usdPerUnit for $base_symbol" >&2; exit 1; } ;; esac quote_units="$(awk -v base="$base_units" -v usd="$usd_per_unit" -v usd_eth="$usd_per_eth" 'BEGIN { printf "%.0f", ((base / 1000000) * usd / usd_eth) * 1000000000000000000 }')" read -r current_base_reserve current_quote_reserve < <(get_pool_reserves "$pool") base_delta="$(big_sub_if_positive "$base_units" "$current_base_reserve")" quote_delta="$(big_sub_if_positive "$quote_units" "$current_quote_reserve")" base_balance="$(cast call "$base_address" 'balanceOf(address)(uint256)' "$DEPLOYER" --rpc-url "$RPC_URL_138" 2>/dev/null || echo 0)" base_balance="$(uint_clean "$base_balance")" owner_addr="$(cast call "$base_address" 'owner()(address)' --rpc-url "$RPC_URL_138" 2>/dev/null || echo "$ZERO_ADDR")" ROWS+=("$base_symbol|$base_address|$pool|$base_units|$quote_units|$current_base_reserve|$current_quote_reserve|$base_delta|$quote_delta|$base_balance|$owner_addr") total_quote_units="$(big_add "$total_quote_units" "$quote_delta")" done < <(jq -r '.explicitPairs[] | [.baseSymbol, (.baseSymbol as $s | .baseSymbol)] | @tsv' "$EXECUTION_CONFIG" | while IFS=$'\t' read -r base_symbol _; do base_address="$(jq -r --arg sym "$base_symbol" '.tokens[$sym]' "$EXECUTION_CONFIG")" printf '%s\t%s\n' "$base_symbol" "$base_address" done) weth_balance="$(cast call "$WETH_ADDRESS" 'balanceOf(address)(uint256)' "$DEPLOYER" --rpc-url "$RPC_URL_138" 2>/dev/null || echo 0)" weth_balance="$(uint_clean "$weth_balance")" eth_balance="$(cast balance "$DEPLOYER" --rpc-url "$RPC_URL_138" 2>/dev/null || echo 0)" if [[ "$(big_lt "$weth_balance" "$total_quote_units")" == "1" ]]; then wrap_needed="$(big_sub_if_positive "$total_quote_units" "$weth_balance")" fi log "Current ETH: $(raw_to_human_18 "$eth_balance")" log "Current WETH: $(raw_to_human_18 "$weth_balance")" log "Total WETH needed for profile: $(raw_to_human_18 "$total_quote_units")" log "Additional WETH to wrap: $(raw_to_human_18 "$wrap_needed")" log "" for row in "${ROWS[@]}"; do IFS='|' read -r base_symbol base_address pool base_units quote_units current_base_reserve current_quote_reserve base_delta quote_delta base_balance owner_addr <<< "$row" need_mint=0 if [[ "$(big_lt "$base_balance" "$base_delta")" == "1" ]]; then need_mint="$(big_sub_if_positive "$base_delta" "$base_balance")" fi log "$base_symbol pool=$pool" log " target base: $(raw_to_human_6 "$base_units")" log " target quote: $(raw_to_human_18 "$quote_units") WETH" log " current base: $(raw_to_human_6 "$current_base_reserve")" log " current quote: $(raw_to_human_18 "$current_quote_reserve") WETH" log " top-up base: $(raw_to_human_6 "$base_delta")" log " top-up quote: $(raw_to_human_18 "$quote_delta") WETH" log " base balance: $(raw_to_human_6 "$base_balance")" if [[ "$need_mint" != "0" ]]; then log " mint needed: $(raw_to_human_6 "$need_mint")" else log " mint needed: no" fi done if [[ "$EXECUTE" != "1" ]]; then log "" log "Dry run only. Re-run with EXECUTE=1 to mint/wrap/approve/add liquidity." exit 0 fi if [[ "$wrap_needed" != "0" ]]; then if [[ "$WRAP_WHEN_NEEDED" != "true" ]]; then echo "Need additional WETH but wrapNativeEthWhenNeeded=false" >&2 exit 1 fi if [[ "$(big_lt "$eth_balance" "$wrap_needed")" == "1" ]]; then echo "Insufficient ETH to wrap required WETH" >&2 exit 1 fi log "Wrapping $(raw_to_human_18 "$wrap_needed") ETH into WETH" send_tx "$WETH_ADDRESS" 'deposit()' --value "$wrap_needed" --gas-limit "$WRAP_GAS_LIMIT" >/dev/null fi for row in "${ROWS[@]}"; do IFS='|' read -r base_symbol base_address pool base_units quote_units current_base_reserve current_quote_reserve base_delta quote_delta base_balance owner_addr <<< "$row" if [[ "$base_delta" == "0" && "$quote_delta" == "0" ]]; then log "Skipping $base_symbol/WETH; already at or above target." continue fi if [[ "$(big_lt "$base_balance" "$base_delta")" == "1" ]]; then mint_amount="$(big_sub_if_positive "$base_delta" "$base_balance")" if [[ "$MINT_WHEN_OWNER" != "true" ]]; then echo "Need to mint $base_symbol but mintMissingBaseWhenOwner=false" >&2 exit 1 fi if [[ "${owner_addr,,}" != "${DEPLOYER,,}" ]]; then echo "Cannot mint $base_symbol; deployer is not owner ($owner_addr)" >&2 exit 1 fi log "Minting $(raw_to_human_6 "$mint_amount") $base_symbol" send_tx "$base_address" 'mint(address,uint256)' "$DEPLOYER" "$mint_amount" --gas-limit "$MINT_GAS_LIMIT" >/dev/null fi if [[ "$APPROVE_MAX" == "true" ]]; then log "Approving $base_symbol to integration" send_tx "$base_address" 'approve(address,uint256)' "$DODO_PMM_INTEGRATION_ADDRESS" "$MAX_UINT" --gas-limit "$APPROVE_GAS_LIMIT" >/dev/null log "Approving WETH for $base_symbol pair" send_tx "$WETH_ADDRESS" 'approve(address,uint256)' "$DODO_PMM_INTEGRATION_ADDRESS" "$MAX_UINT" --gas-limit "$APPROVE_GAS_LIMIT" >/dev/null fi log "Adding liquidity to $base_symbol/WETH at $pool" send_tx "$DODO_PMM_INTEGRATION_ADDRESS" 'addLiquidity(address,uint256,uint256)' "$pool" "$base_delta" "$quote_delta" --gas-limit "$ADD_LIQUIDITY_GAS_LIMIT" >/dev/null done log "" log "Liquidity funding complete."