#!/usr/bin/env bash set -euo pipefail # Verify the private Chain 138 DODO v3 / D3MM rollout. # # Checks: # 1. Canonical D3MM pool returns the expected version string. # 2. D3Vault still recognizes the canonical pool. # 3. D3Oracle has a non-zero, whitelisted WETH10 source. # 4. A quote probe for WETH10 -> USDT returns a healthy amount. # 5. Core DODO v3 pilot contracts remain source-verified on Blockscout. # # Optional env overrides: # RPC_URL_138 # CHAIN138_D3_ORACLE_ADDRESS # CHAIN138_D3_VAULT_ADDRESS # CHAIN138_D3_MM_FACTORY_ADDRESS # CHAIN138_D3_PROXY_ADDRESS # CHAIN138_D3_DODO_APPROVE_ADDRESS # CHAIN138_D3_DODO_APPROVE_PROXY_ADDRESS # CHAIN138_D3_MM_ADDRESS # CHAIN138_D3_WETH10_ADDRESS # CHAIN138_D3_WETH_USD_FEED # CHAIN138_D3_BOOTSTRAP_WETH_USD_MOCK # CHAIN138_D3_USDT_ADDRESS # CHAIN138_D3_USDC_ADDRESS # CHAIN138_D3_CUSDT_ADDRESS # CHAIN138_D3_CUSDC_ADDRESS # CHAIN138_D3_USDT_USD_FEED # CHAIN138_D3_USDC_USD_FEED # CHAIN138_D3_CUSDT_USD_FEED # CHAIN138_D3_CUSDC_USD_FEED # CHAIN138_D3_ALLOW_BOOTSTRAP_STABLE_MOCKS=1 to downgrade old stable-feed addresses to warnings # CHAIN138_D3_PROBE_SELL_AMOUNT # CHAIN138_D3_MIN_QUOTE_OUT # CHAIN138_BLOCKSCOUT_API_BASE SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" if [[ -f "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" ]]; then # shellcheck source=/dev/null source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" fi RPC_URL="${RPC_URL_138:-${CHAIN138_RPC_URL:-${CHAIN138_RPC:-http://192.168.11.211:8545}}}" D3_ORACLE="${CHAIN138_D3_ORACLE_ADDRESS:-0xD7459aEa8bB53C83a1e90262777D730539A326F0}" D3_VAULT="${CHAIN138_D3_VAULT_ADDRESS:-0x42b6867260Fb9eE6d09B7E0233A1fAD65D0133D1}" D3_FACTORY="${CHAIN138_D3_MM_FACTORY_ADDRESS:-0x78470C7d2925B6738544E2DD4FE7c07CcA21AC31}" D3_PROXY="${CHAIN138_D3_PROXY_ADDRESS:-0xc9a11abB7C63d88546Be24D58a6d95e3762cB843}" DODO_APPROVE="${CHAIN138_D3_DODO_APPROVE_ADDRESS:-0xbF8D5CB7E8F333CA686a27374Ae06F5dfd772E9E}" DODO_APPROVE_PROXY="${CHAIN138_D3_DODO_APPROVE_PROXY_ADDRESS:-0x08d764c03C42635d8ef9046752b5694243E21Fe9}" D3MM="${CHAIN138_D3_MM_ADDRESS:-0x6550A3a59070061a262a893A1D6F3F490afFDBDA}" WETH10="${CHAIN138_D3_WETH10_ADDRESS:-0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9F}" WETH_USD_FEED="${CHAIN138_D3_WETH_USD_FEED:-0x99b3511a2d315a497c8112c1fdd8d508d4b1e506}" BOOTSTRAP_WETH_MOCK="${CHAIN138_D3_BOOTSTRAP_WETH_USD_MOCK:-0xf1bbBc7FCBB96C6a143C752d4DAEFA34e192EE9c}" USDT="${CHAIN138_D3_USDT_ADDRESS:-0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1}" USDC="${CHAIN138_D3_USDC_ADDRESS:-0x71D6687F38b93CCad569Fa6352c876eea967201b}" cUSDT="${CHAIN138_D3_CUSDT_ADDRESS:-0x93E66202A11B1772E55407B32B44e5Cd8eda7f22}" cUSDC="${CHAIN138_D3_CUSDC_ADDRESS:-0xf22258f57794CC8E06237084b353Ab30fFfa640b}" USDT_USD_FEED="${CHAIN138_D3_USDT_USD_FEED:-0x7c2Cb2667f0f97f4004aae04B67d94A085E6f0f1}" USDC_USD_FEED="${CHAIN138_D3_USDC_USD_FEED:-0xf072Ac13D45e6c83296ca18F3E04185B747DD6aa}" cUSDT_USD_FEED="${CHAIN138_D3_CUSDT_USD_FEED:-0x7c96E66F4a0713e327F9e73Cf2721f13DB29036C}" cUSDC_USD_FEED="${CHAIN138_D3_CUSDC_USD_FEED:-0x291694095232CA80077125F64f6f73076e7910C1}" BOOTSTRAP_USDT_MOCK="${CHAIN138_D3_BOOTSTRAP_USDT_USD_MOCK:-0x8c5eD794399C68468985238Fa127958E06e6e87F}" BOOTSTRAP_USDC_MOCK="${CHAIN138_D3_BOOTSTRAP_USDC_USD_MOCK:-0x102BAe3eBf3C8B93445F4B30c90728fd933CeC84}" BOOTSTRAP_cUSDT_MOCK="${CHAIN138_D3_BOOTSTRAP_CUSDT_USD_MOCK:-0xd493Bb05e9ddf954E8285c6358ED03a2672A006d}" BOOTSTRAP_cUSDC_MOCK="${CHAIN138_D3_BOOTSTRAP_CUSDC_USD_MOCK:-0xcA85aFc0E24A8ebd69E138928567DcD363758E7A}" ALLOW_BOOTSTRAP_STABLE_MOCKS="${CHAIN138_D3_ALLOW_BOOTSTRAP_STABLE_MOCKS:-0}" PROBE_AMOUNT="${CHAIN138_D3_PROBE_SELL_AMOUNT:-100000000000000000}" MIN_QUOTE_OUT="${CHAIN138_D3_MIN_QUOTE_OUT:-100000000}" BLOCKSCOUT_INTERNAL_API_BASE="${CHAIN138_BLOCKSCOUT_INTERNAL_API_BASE:-http://${IP_BLOCKSCOUT:-192.168.11.140}:4000/api/v2}" BLOCKSCOUT_PUBLIC_API_BASE="${CHAIN138_BLOCKSCOUT_PUBLIC_API_BASE:-https://explorer.d-bis.org/api/v2}" log() { printf '%s\n' "$*" } ok() { printf '[ok] %s\n' "$*" } warn() { printf '[warn] %s\n' "$*" >&2 } fail() { printf '[fail] %s\n' "$*" >&2 exit 1 } format_units() { local value="$1" local decimals="$2" if [[ "${value}" =~ ^- ]]; then local positive="${value#-}" printf -- "-%s" "$(format_units "${positive}" "${decimals}")" return 0 fi if [[ "${decimals}" -eq 0 ]]; then printf '%s' "${value}" return 0 fi local len="${#value}" if (( len <= decimals )); then local pad_count=$((decimals - len)) local zeros="" if (( pad_count > 0 )); then zeros=$(printf '%0*d' "${pad_count}" 0) fi local fraction="${zeros}${value}" fraction="${fraction%"${fraction##*[!0]}"}" if [[ -z "${fraction}" ]]; then printf '0' else printf '0.%s' "${fraction}" fi return 0 fi local whole="${value:0:len-decimals}" local fraction="${value:len-decimals}" fraction="${fraction%"${fraction##*[!0]}"}" if [[ -z "${fraction}" ]]; then printf '%s' "${whole}" else printf '%s.%s' "${whole}" "${fraction}" fi } require_command() { command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" } require_command cast if command -v curl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then HAS_BLOCKSCOUT_TOOLS=1 else HAS_BLOCKSCOUT_TOOLS=0 fi check_stable_feed() { local label="$1" local token_address="$2" local expected_feed="$3" local bootstrap_feed="$4" local expected_desc="$5" mapfile -t source < <( cast call --rpc-url "${RPC_URL}" "${D3_ORACLE}" \ "priceSources(address)(address,bool,uint256,uint8,uint8,uint256)" "${token_address}" ) || fail "Failed to read ${label} price source from D3Oracle." (( ${#source[@]} >= 6 )) || fail "Unexpected ${label} priceSources response length: ${#source[@]}" local feed_address="${source[0]}" local whitelisted="${source[1]}" [[ "${feed_address}" != "0x0000000000000000000000000000000000000000" ]] || fail "${label} feed address is zero." [[ "${whitelisted}" == "true" ]] || fail "${label} feed is not whitelisted." if [[ "${feed_address,,}" == "${bootstrap_feed,,}" ]]; then if [[ "${ALLOW_BOOTSTRAP_STABLE_MOCKS}" == "1" ]]; then warn "${label} still points at bootstrap mock feed ${bootstrap_feed}." else fail "${label} still points at bootstrap mock feed ${bootstrap_feed}." fi fi [[ "${feed_address,,}" == "${expected_feed,,}" ]] || fail "${label} expected managed feed ${expected_feed}, got ${feed_address}." local description description="$(cast call --rpc-url "${RPC_URL}" "${feed_address}" "description()(string)")" || fail "Failed to read ${label} feed description." [[ "${description}" == "\"${expected_desc}\"" ]] || fail "${label} feed description mismatch: ${description}" mapfile -t round_data < <( cast call --rpc-url "${RPC_URL}" "${feed_address}" \ "latestRoundData()(uint80,int256,uint256,uint256,uint80)" ) || fail "Failed to read ${label} latestRoundData." (( ${#round_data[@]} >= 5 )) || fail "Unexpected ${label} latestRoundData length: ${#round_data[@]}" local answer="${round_data[1]%% *}" [[ "${answer}" == "100000000" ]] || fail "${label} expected 1.00000000 USD answer, got ${answer}." ok "${label} uses managed feed ${feed_address} (${expected_desc}) with answer ${answer}." } check_blockscout_verified() { local label="$1" local addr="$2" local expected_name="$3" local json name compiler base json="" for base in "${BLOCKSCOUT_INTERNAL_API_BASE}" "${BLOCKSCOUT_PUBLIC_API_BASE}"; do for _ in 1 2 3; do json="$(curl --max-time 15 -fsS "${base}/smart-contracts/${addr}" 2>/dev/null || true)" if [[ -n "${json}" ]] && jq -e 'type == "object"' >/dev/null 2>&1 <<<"${json}"; then break 2 fi json="" sleep 2 done done [[ -n "${json}" ]] || fail "Failed to read Blockscout metadata for ${label} (${addr})." name="$(jq -r '.name // empty' <<<"${json}")" compiler="$(jq -r '.compiler_version // empty' <<<"${json}")" [[ -n "${name}" ]] || fail "${label} is not source-verified on Blockscout." [[ -n "${compiler}" ]] || fail "${label} is missing compiler metadata on Blockscout." [[ "${name}" == "${expected_name}" ]] || fail "${label} expected Blockscout name ${expected_name}, got ${name}." ok "${label} is Blockscout-verified as ${name} (${compiler})." } check_blockscout_verified_or_warn_bytecode_only() { local label="$1" local addr="$2" local expected_name="$3" local json name compiler base json="" for base in "${BLOCKSCOUT_INTERNAL_API_BASE}" "${BLOCKSCOUT_PUBLIC_API_BASE}"; do for _ in 1 2 3; do json="$(curl --max-time 15 -fsS "${base}/smart-contracts/${addr}" 2>/dev/null || true)" if [[ -n "${json}" ]] && jq -e 'type == "object"' >/dev/null 2>&1 <<<"${json}"; then break 2 fi json="" sleep 2 done done [[ -n "${json}" ]] || fail "Failed to read Blockscout metadata for ${label} (${addr})." name="$(jq -r '.name // empty' <<<"${json}")" compiler="$(jq -r '.compiler_version // empty' <<<"${json}")" if [[ -n "${name}" && -n "${compiler}" && "${name}" == "${expected_name}" ]]; then ok "${label} is Blockscout-verified as ${name} (${compiler})." return 0 fi if jq -e '.creation_bytecode and .deployed_bytecode' >/dev/null 2>&1 <<<"${json}"; then warn "${label} currently exposes only bytecode metadata on Blockscout; source-verification submission exists but explorer metadata has not fully materialized." return 0 fi fail "${label} is not source-verified on Blockscout." } log "Chain 138 DODO v3 verification" log "RPC: ${RPC_URL}" log "D3Oracle: ${D3_ORACLE}" log "D3Vault: ${D3_VAULT}" log "D3MMFactory: ${D3_FACTORY}" log "D3Proxy: ${D3_PROXY}" log "Canonical D3MM: ${D3MM}" log version="$(cast call --rpc-url "${RPC_URL}" "${D3MM}" "version()(string)")" || fail "Failed to read D3MM version." [[ "${version}" == "\"D3MM 1.0.0\"" ]] || fail "Unexpected D3MM version: ${version}" ok "Canonical pool reports version ${version}." vault_recognizes_pool="$(cast call --rpc-url "${RPC_URL}" "${D3_VAULT}" "allPoolAddrMap(address)(bool)" "${D3MM}")" || fail "Failed to read vault pool map." [[ "${vault_recognizes_pool}" == "true" ]] || fail "D3Vault does not recognize canonical D3MM ${D3MM}." ok "D3Vault recognizes the canonical D3MM." mapfile -t oracle_source < <( cast call --rpc-url "${RPC_URL}" "${D3_ORACLE}" \ "priceSources(address)(address,bool,uint256,uint8,uint8,uint256)" "${WETH10}" ) || fail "Failed to read WETH10 price source from D3Oracle." (( ${#oracle_source[@]} >= 6 )) || fail "Unexpected D3Oracle priceSources response length: ${#oracle_source[@]}" oracle_address="${oracle_source[0]}" oracle_whitelisted="${oracle_source[1]}" price_tolerance="${oracle_source[2]%% *}" price_decimals="${oracle_source[3]%% *}" token_decimals="${oracle_source[4]%% *}" heartbeat="${oracle_source[5]%% *}" [[ "${oracle_address}" != "0x0000000000000000000000000000000000000000" ]] || fail "WETH10 oracle address is zero." [[ "${oracle_whitelisted}" == "true" ]] || fail "WETH10 oracle is not whitelisted." [[ "${oracle_address,,}" != "${BOOTSTRAP_WETH_MOCK,,}" ]] || fail "WETH10 still points at bootstrap mock feed ${BOOTSTRAP_WETH_MOCK}." [[ "${oracle_address,,}" == "${WETH_USD_FEED,,}" ]] || fail "WETH10 expected oracle feed ${WETH_USD_FEED}, got ${oracle_address}." (( heartbeat >= 100000 )) || fail "WETH10 heartbeat ${heartbeat} is unexpectedly low." weth_desc="$(cast call --rpc-url "${RPC_URL}" "${oracle_address}" "description()(string)")" || fail "Failed to read WETH10 feed description." [[ "${weth_desc}" == "\"ETH/USD Price Feed\"" ]] || fail "Unexpected WETH10 feed description: ${weth_desc}" ok "WETH10 oracle is configured at ${oracle_address} (${weth_desc}, tolerance=${price_tolerance}, priceDecimals=${price_decimals}, tokenDecimals=${token_decimals}, heartbeat=${heartbeat})." mapfile -t quote_probe < <( cast call --rpc-url "${RPC_URL}" "${D3MM}" \ "querySellTokens(address,address,uint256)(uint256,uint256,uint256,uint256,uint256)" \ "${WETH10}" "${USDT}" "${PROBE_AMOUNT}" ) || fail "Failed to query WETH10 -> USDT on canonical D3MM." (( ${#quote_probe[@]} >= 5 )) || fail "Unexpected D3MM quote response length: ${#quote_probe[@]}" pay_from="${quote_probe[0]%% *}" receive_to="${quote_probe[1]%% *}" vusd_amount="${quote_probe[2]%% *}" swap_fee="${quote_probe[3]%% *}" mt_fee="${quote_probe[4]%% *}" [[ "${pay_from}" == "${PROBE_AMOUNT}" ]] || fail "D3MM returned unexpected payFromAmount ${pay_from} for probe amount ${PROBE_AMOUNT}." [[ "${receive_to}" != "0" ]] || fail "Canonical D3MM returned zero receive amount for WETH10 -> USDT." if [[ "${receive_to}" =~ ^[0-9]+$ ]] && [[ "${MIN_QUOTE_OUT}" =~ ^[0-9]+$ ]]; then if (( receive_to < MIN_QUOTE_OUT )); then fail "Canonical D3MM quote ${receive_to} is below expected floor ${MIN_QUOTE_OUT}." fi else warn "Skipping numeric threshold comparison because quote or floor was not numeric." fi ok "Quote probe WETH10 -> USDT for $(format_units "${PROBE_AMOUNT}" 18) WETH10 returned $(format_units "${receive_to}" 6) USDT (vusd=${vusd_amount}, swapFee=$(format_units "${swap_fee}" 6), mtFee=$(format_units "${mt_fee}" 6))." check_stable_feed "USDT" "${USDT}" "${USDT_USD_FEED}" "${BOOTSTRAP_USDT_MOCK}" "USDT / USD" check_stable_feed "USDC" "${USDC}" "${USDC_USD_FEED}" "${BOOTSTRAP_USDC_MOCK}" "USDC / USD" check_stable_feed "cUSDT" "${cUSDT}" "${cUSDT_USD_FEED}" "${BOOTSTRAP_cUSDT_MOCK}" "cUSDT / USD" check_stable_feed "cUSDC" "${cUSDC}" "${cUSDC_USD_FEED}" "${BOOTSTRAP_cUSDC_MOCK}" "cUSDC / USD" if [[ "${HAS_BLOCKSCOUT_TOOLS}" == "1" ]]; then check_blockscout_verified "D3Oracle" "${D3_ORACLE}" "D3Oracle" check_blockscout_verified "D3Vault" "${D3_VAULT}" "D3Vault" check_blockscout_verified_or_warn_bytecode_only "D3MMFactory" "${D3_FACTORY}" "D3MMFactory" check_blockscout_verified_or_warn_bytecode_only "D3Proxy" "${D3_PROXY}" "D3Proxy" check_blockscout_verified "DODOApprove" "${DODO_APPROVE}" "DODOApprove" check_blockscout_verified "DODOApproveProxy" "${DODO_APPROVE_PROXY}" "DODOApproveProxy" else warn "Skipping Blockscout source-verification checks because curl or jq is missing." fi log log "Result: Chain 138 DODO v3 canonical WETH10 pool and managed stable feeds look healthy."