Files
proxmox/scripts/verify/check-gas-rollout-deployment-matrix.sh
defiQUG dbd517b279 Sync workspace: config, docs, scripts, CI, operator rules, and submodule pointers.
- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains
- Omit embedded publish git dirs and empty placeholders from index

Made-with: Cursor
2026-04-12 06:12:20 -07:00

455 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
# Audit live gas-native rollout prerequisites and print the next deploy/config steps.
# Usage:
# bash scripts/verify/check-gas-rollout-deployment-matrix.sh
# bash scripts/verify/check-gas-rollout-deployment-matrix.sh --json
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
export PROJECT_ROOT
# shellcheck source=/dev/null
source "$PROJECT_ROOT/scripts/lib/load-project-env.sh" >/dev/null 2>&1 || true
OUTPUT_JSON=0
for arg in "$@"; do
case "$arg" in
--json) OUTPUT_JSON=1 ;;
*)
echo "Unknown argument: $arg" >&2
exit 2
;;
esac
done
command -v node >/dev/null 2>&1 || {
echo "[FAIL] Missing required command: node" >&2
exit 1
}
command -v cast >/dev/null 2>&1 || {
echo "[FAIL] Missing required command: cast" >&2
exit 1
}
OUTPUT_JSON="$OUTPUT_JSON" node <<'NODE'
const path = require('path');
const { execFileSync } = require('child_process');
const loader = require(path.join(process.env.PROJECT_ROOT, 'config/token-mapping-loader.cjs'));
const contracts = require(path.join(process.env.PROJECT_ROOT, 'config/contracts-loader.cjs'));
const outputJson = process.env.OUTPUT_JSON === '1';
const active = loader.loadGruTransportActiveJson() || {};
const families = loader.getGasAssetFamilies();
const pairs = loader.getActiveTransportPairs().filter((pair) => pair.assetClass === 'gas_native');
const reserveVerifiers = active.reserveVerifiers || {};
const deployedGenericVerifierAddress = contracts.getContractAddress(138, 'CWAssetReserveVerifier') || '';
const configuredGasPairs = Array.isArray(active.transportPairs)
? active.transportPairs.filter((pair) => pair && pair.assetClass === 'gas_native')
: [];
const deferredGasPairs = configuredGasPairs.filter((pair) => pair.active === false).length;
const rpcEnvByChain = {
138: 'RPC_URL_138',
1: 'ETHEREUM_MAINNET_RPC',
10: 'OPTIMISM_MAINNET_RPC',
25: 'CRONOS_RPC_URL',
56: 'BSC_RPC_URL',
100: 'GNOSIS_MAINNET_RPC',
137: 'POLYGON_MAINNET_RPC',
42161: 'ARBITRUM_MAINNET_RPC',
42220: 'CELO_MAINNET_RPC',
43114: 'AVALANCHE_RPC_URL',
8453: 'BASE_MAINNET_RPC',
1111: 'WEMIX_RPC',
};
function resolveConfigRef(ref) {
if (!ref || typeof ref !== 'object') return '';
if (typeof ref.address === 'string' && ref.address.trim()) return ref.address.trim();
if (typeof ref.env === 'string' && process.env[ref.env]) return process.env[ref.env];
return '';
}
function hasCode(address, rpcUrl) {
if (!address || !/^0x[a-fA-F0-9]{40}$/.test(address) || !rpcUrl) return null;
try {
const out = execFileSync('cast', ['code', address, '--rpc-url', rpcUrl], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
return out !== '' && out !== '0x';
} catch {
return false;
}
}
function callRead(address, rpcUrl, signature, args = []) {
if (!address || !/^0x[a-fA-F0-9]{40}$/.test(address) || !rpcUrl) {
return { ok: false, skipped: true, value: null, error: 'missing_rpc_or_address' };
}
try {
const out = execFileSync(
'cast',
['call', address, signature, ...args.map((arg) => String(arg)), '--rpc-url', rpcUrl],
{
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}
).trim();
return { ok: true, skipped: false, value: out, error: null };
} catch (error) {
const stderr = error && error.stderr ? String(error.stderr).trim() : '';
const stdout = error && error.stdout ? String(error.stdout).trim() : '';
return {
ok: false,
skipped: false,
value: null,
error: stderr || stdout || (error && error.message ? String(error.message).trim() : 'cast_call_failed'),
};
}
}
function parseDestinationConfig(rawValue) {
if (typeof rawValue !== 'string') {
return { receiverBridge: null, enabled: null };
}
const match = rawValue.match(/^\((0x[a-fA-F0-9]{40}),\s*(true|false)\)$/);
if (!match) {
return { receiverBridge: null, enabled: null };
}
return {
receiverBridge: match[1],
enabled: match[2] === 'true',
};
}
function classifyL1BridgeCapability(probe) {
const adminReadable = probe.admin.ok;
const destinationReadable = probe.destination.ok;
const accountingReadable =
probe.reserveVerifier.ok &&
probe.supportedCanonicalToken.ok &&
probe.maxOutstanding.ok &&
probe.outstandingMinted.ok &&
probe.totalOutstanding.ok &&
probe.lockedBalance.ok;
if (adminReadable && destinationReadable && accountingReadable) return 'full_accounting';
if (adminReadable && destinationReadable) return 'partial_destination_only';
if (adminReadable) return 'admin_only';
if (probe.hasCode === false) return 'missing';
return 'unknown_or_incompatible';
}
function probeL1Bridge(address, rpcUrl, canonicalAddress, destinationChainSelector) {
const admin = callRead(address, rpcUrl, 'admin()(address)');
const destination =
canonicalAddress && destinationChainSelector
? callRead(address, rpcUrl, 'destinations(address,uint64)((address,bool))', [
canonicalAddress,
destinationChainSelector,
])
: { ok: false, skipped: true, value: null, error: 'missing_selector_or_token' };
const reserveVerifier = callRead(address, rpcUrl, 'reserveVerifier()(address)');
const supportedCanonicalToken = canonicalAddress
? callRead(address, rpcUrl, 'supportedCanonicalToken(address)(bool)', [canonicalAddress])
: { ok: false, skipped: true, value: null, error: 'missing_token' };
const maxOutstanding =
canonicalAddress && destinationChainSelector
? callRead(address, rpcUrl, 'maxOutstanding(address,uint64)(uint256)', [canonicalAddress, destinationChainSelector])
: { ok: false, skipped: true, value: null, error: 'missing_selector_or_token' };
const outstandingMinted =
canonicalAddress && destinationChainSelector
? callRead(address, rpcUrl, 'outstandingMinted(address,uint64)(uint256)', [
canonicalAddress,
destinationChainSelector,
])
: { ok: false, skipped: true, value: null, error: 'missing_selector_or_token' };
const totalOutstanding = canonicalAddress
? callRead(address, rpcUrl, 'totalOutstanding(address)(uint256)', [canonicalAddress])
: { ok: false, skipped: true, value: null, error: 'missing_token' };
const lockedBalance = canonicalAddress
? callRead(address, rpcUrl, 'lockedBalance(address)(uint256)', [canonicalAddress])
: { ok: false, skipped: true, value: null, error: 'missing_token' };
const destinationConfig = parseDestinationConfig(destination.value);
const readableViews = [
['admin', admin.ok],
['destinations', destination.ok],
['reserveVerifier', reserveVerifier.ok],
['supportedCanonicalToken', supportedCanonicalToken.ok],
['maxOutstanding', maxOutstanding.ok],
['outstandingMinted', outstandingMinted.ok],
['totalOutstanding', totalOutstanding.ok],
['lockedBalance', lockedBalance.ok],
]
.filter(([, readable]) => readable)
.map(([label]) => label);
return {
capability: classifyL1BridgeCapability({
hasCode: hasCode(address, rpcUrl),
admin,
destination,
reserveVerifier,
supportedCanonicalToken,
maxOutstanding,
outstandingMinted,
totalOutstanding,
lockedBalance,
}),
readableViews,
destinationConfigured: destinationConfig.enabled,
destinationReceiverBridge: destinationConfig.receiverBridge,
reserveVerifier,
supportedCanonicalToken,
maxOutstanding,
outstandingMinted,
totalOutstanding,
lockedBalance,
};
}
function describeMirrorName(pair) {
if (pair.familyKey === 'eth_mainnet') return 'Wrapped cETH Mainnet';
if (pair.familyKey === 'eth_l2') return 'Wrapped cETHL2';
return `Wrapped ${pair.canonicalSymbol}`;
}
function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
const rows = pairs.map((pair) => {
const destinationRpcEnv = rpcEnvByChain[pair.destinationChainId] || null;
const destinationRpcUrl = destinationRpcEnv ? process.env[destinationRpcEnv] || '' : '';
const chain138RpcUrl = process.env[rpcEnvByChain[138]] || '';
const reserveVerifier = pair.reserveVerifierKey ? reserveVerifiers[pair.reserveVerifierKey] || null : null;
const reserveVerifierEnvKey =
reserveVerifier &&
reserveVerifier.verifierRef &&
typeof reserveVerifier.verifierRef.env === 'string' &&
reserveVerifier.verifierRef.env.trim()
? reserveVerifier.verifierRef.env.trim()
: '';
const l1BridgeAddress = pair.runtimeL1BridgeAddress || resolveConfigRef(pair.peer?.l1Bridge);
const l2BridgeAddress = pair.runtimeL2BridgeAddress || resolveConfigRef(pair.peer?.l2Bridge);
const reserveVerifierAddress = pair.runtimeReserveVerifierAddress || resolveConfigRef(reserveVerifier?.verifierRef);
const reserveVaultAddress = pair.runtimeReserveVaultAddress || resolveConfigRef(reserveVerifier?.vaultRef);
const destinationChainSelector = typeof pair.destinationChainSelector === 'string' ? pair.destinationChainSelector : '';
const canonicalCodeLive = hasCode(pair.canonicalAddress, chain138RpcUrl);
const mirroredCodeLive = hasCode(pair.mirroredAddress, destinationRpcUrl);
const l1BridgeLive = hasCode(l1BridgeAddress, chain138RpcUrl);
const l2BridgeLive = hasCode(l2BridgeAddress, destinationRpcUrl);
const reserveVerifierLive = hasCode(reserveVerifierAddress, chain138RpcUrl);
const reserveVaultLive = reserveVaultAddress ? hasCode(reserveVaultAddress, chain138RpcUrl) : null;
const l1BridgeProbe = probeL1Bridge(l1BridgeAddress, chain138RpcUrl, pair.canonicalAddress, destinationChainSelector);
const destinationReceiverMatchesRuntimeL2 =
!!l1BridgeProbe.destinationReceiverBridge &&
!!l2BridgeAddress &&
l1BridgeProbe.destinationReceiverBridge.toLowerCase() === l2BridgeAddress.toLowerCase();
const actions = [];
if (canonicalCodeLive !== true) {
actions.push({
type: 'deployCanonical',
command:
`GAS_FAMILY=${shellQuote(pair.familyKey)} forge script script/deploy/DeployGasCanonicalTokens.s.sol:DeployGasCanonicalTokens --rpc-url "$RPC_URL_138" --broadcast`,
});
}
if (mirroredCodeLive !== true) {
actions.push({
type: 'deployMirror',
command:
`CW_BRIDGE_ADDRESS="$${pair.peer?.l2Bridge?.env || 'CW_BRIDGE_MISSING'}" ` +
`CW_TOKEN_NAME=${shellQuote(describeMirrorName(pair))} ` +
`CW_TOKEN_SYMBOL=${shellQuote(pair.mirroredSymbol)} ` +
`CW_TOKEN_DECIMALS=18 ` +
`forge script script/deploy/DeploySingleCWToken.s.sol:DeploySingleCWToken --rpc-url "$${destinationRpcEnv || 'RPC_MISSING'}" --broadcast`,
});
}
if (!l1BridgeAddress) {
actions.push({ type: 'setEnv', command: 'export CHAIN138_L1_BRIDGE=<live_chain138_l1_bridge>' });
}
if (!l2BridgeAddress) {
actions.push({ type: 'setEnv', command: `export ${pair.peer?.l2Bridge?.env || 'CW_BRIDGE_TARGET'}=<live_destination_bridge>` });
}
if (destinationChainSelector && l1BridgeProbe.destinationConfigured !== true && l2BridgeAddress) {
actions.push({
type: 'configureL1Destination',
command:
`cast send "$CHAIN138_L1_BRIDGE" "configureDestination(address,uint64,address,bool)" ` +
`${pair.canonicalAddress} ${destinationChainSelector} ${l2BridgeAddress} true ` +
'--rpc-url "$RPC_URL_138" --private-key "$PRIVATE_KEY"',
});
}
if (!reserveVerifierAddress && deployedGenericVerifierAddress && reserveVerifierEnvKey) {
actions.push({
type: 'setVerifierEnv',
command: `export ${reserveVerifierEnvKey}=${deployedGenericVerifierAddress}`,
});
} else if (!reserveVerifierAddress) {
actions.push({
type: 'deployVerifier',
command:
'CW_ASSET_RESERVE_VAULT=<live_vault> CW_ASSET_RESERVE_SYSTEM=<live_reserve_system_or_blank> ' +
'forge script script/DeployCWAssetReserveVerifier.s.sol:DeployCWAssetReserveVerifier --rpc-url "$RPC_URL_138" --broadcast',
});
}
if (!pair.runtimeMaxOutstandingValue) {
actions.push({ type: 'setCap', command: `export ${pair.maxOutstanding?.env || 'CW_MAX_OUTSTANDING_TARGET'}=<per_lane_cap_raw>` });
}
if (!pair.runtimeOutstandingValue || !pair.runtimeEscrowedValue || pair.runtimeTreasuryBackedValue == null) {
actions.push({ type: 'setSupplyAccounting', command: 'export CW_GAS_OUTSTANDING_*=... CW_GAS_ESCROWED_*=... CW_GAS_TREASURY_BACKED_*=...' });
}
if (l1BridgeProbe.capability !== 'full_accounting' && l1BridgeLive === true) {
actions.push({
type: 'auditL1BridgeAbi',
command:
`cast call "$CHAIN138_L1_BRIDGE" "admin()(address)" --rpc-url "$RPC_URL_138" && ` +
`cast call "$CHAIN138_L1_BRIDGE" "destinations(address,uint64)((address,bool))" ${pair.canonicalAddress} ${destinationChainSelector || 0} --rpc-url "$RPC_URL_138"`,
});
}
return {
key: pair.key,
familyKey: pair.familyKey,
chainId: pair.destinationChainId,
chainName: pair.destinationChainName,
destinationChainSelector: destinationChainSelector || null,
canonicalSymbol: pair.canonicalSymbol,
mirroredSymbol: pair.mirroredSymbol,
canonicalAddress: pair.canonicalAddress,
mirroredAddress: pair.mirroredAddress,
l1BridgeAddress,
l2BridgeAddress,
reserveVerifierAddress,
deployedGenericVerifierAddress: deployedGenericVerifierAddress || null,
reserveVerifierEnvKey: reserveVerifierEnvKey || null,
reserveVaultAddress,
canonicalCodeLive,
mirroredCodeLive,
l1BridgeLive,
l2BridgeLive,
reserveVerifierLive,
reserveVaultLive,
l1BridgeCapability: l1BridgeProbe.capability,
l1BridgeReadableViews: l1BridgeProbe.readableViews,
l1BridgeProbeErrors: {
reserveVerifier: l1BridgeProbe.reserveVerifier.ok ? null : l1BridgeProbe.reserveVerifier.error,
supportedCanonicalToken: l1BridgeProbe.supportedCanonicalToken.ok ? null : l1BridgeProbe.supportedCanonicalToken.error,
maxOutstanding: l1BridgeProbe.maxOutstanding.ok ? null : l1BridgeProbe.maxOutstanding.error,
outstandingMinted: l1BridgeProbe.outstandingMinted.ok ? null : l1BridgeProbe.outstandingMinted.error,
totalOutstanding: l1BridgeProbe.totalOutstanding.ok ? null : l1BridgeProbe.totalOutstanding.error,
lockedBalance: l1BridgeProbe.lockedBalance.ok ? null : l1BridgeProbe.lockedBalance.error,
},
destinationConfiguredOnL1: l1BridgeProbe.destinationConfigured,
destinationReceiverBridgeOnL1: l1BridgeProbe.destinationReceiverBridge,
destinationReceiverMatchesRuntimeL2,
runtimeReady: pair.runtimeReady === true,
runtimeMissingRequirements: pair.runtimeMissingRequirements || [],
actions,
};
});
const uniqueL1BridgeRows = Array.from(
new Map(
rows
.filter((row) => row.l1BridgeAddress)
.map((row) => [String(row.l1BridgeAddress).toLowerCase(), row])
).values()
);
const summary = {
gasFamilies: families.length,
transportPairs: rows.length,
configuredTransportPairs: configuredGasPairs.length,
deferredTransportPairs: deferredGasPairs,
canonicalContractsLive: rows.filter((row) => row.canonicalCodeLive === true).length,
mirroredContractsLive: rows.filter((row) => row.mirroredCodeLive === true).length,
l1BridgeRefsLoaded: rows.filter((row) => row.l1BridgeAddress).length,
l2BridgeRefsLoaded: rows.filter((row) => row.l2BridgeAddress).length,
verifierRefsLoaded: rows.filter((row) => row.reserveVerifierAddress).length,
runtimeReadyPairs: rows.filter((row) => row.runtimeReady).length,
pairsWithL1DestinationConfigured: rows.filter((row) => row.destinationConfiguredOnL1 === true).length,
pairsWithL1ReceiverMatchingRuntimeL2: rows.filter((row) => row.destinationReceiverMatchesRuntimeL2 === true).length,
l1BridgesObserved: uniqueL1BridgeRows.length,
l1BridgesWithFullAccountingIntrospection: uniqueL1BridgeRows.filter(
(row) => row.l1BridgeCapability === 'full_accounting'
).length,
l1BridgesWithPartialDestinationIntrospection: uniqueL1BridgeRows.filter(
(row) => row.l1BridgeCapability === 'partial_destination_only'
).length,
deployedGenericVerifierAddress: deployedGenericVerifierAddress || null,
};
if (outputJson) {
console.log(JSON.stringify({ summary, rows }, null, 2));
process.exit(0);
}
console.log('=== Gas Rollout Deployment Matrix ===');
console.log(`Gas families: ${summary.gasFamilies}`);
console.log(`Active transport pairs: ${summary.transportPairs}`);
console.log(`Deferred transport pairs: ${summary.deferredTransportPairs}`);
console.log(`Canonical contracts live on 138: ${summary.canonicalContractsLive}/${summary.transportPairs}`);
console.log(`Mirrored contracts live on destination chains: ${summary.mirroredContractsLive}/${summary.transportPairs}`);
console.log(`Loaded L1 bridge refs: ${summary.l1BridgeRefsLoaded}/${summary.transportPairs}`);
console.log(`Loaded L2 bridge refs: ${summary.l2BridgeRefsLoaded}/${summary.transportPairs}`);
console.log(`Loaded verifier refs: ${summary.verifierRefsLoaded}/${summary.transportPairs}`);
console.log(`Runtime-ready pairs: ${summary.runtimeReadyPairs}/${summary.transportPairs}`);
console.log(`L1 destinations configured: ${summary.pairsWithL1DestinationConfigured}/${summary.transportPairs}`);
console.log(
`L1 destination receivers matching runtime L2 bridges: ${summary.pairsWithL1ReceiverMatchingRuntimeL2}/${summary.transportPairs}`
);
console.log(
`Observed L1 bridges with full accounting introspection: ${summary.l1BridgesWithFullAccountingIntrospection}/${summary.l1BridgesObserved}`
);
console.log(
`Observed L1 bridges with destination-only introspection: ${summary.l1BridgesWithPartialDestinationIntrospection}/${summary.l1BridgesObserved}`
);
if (summary.deployedGenericVerifierAddress) {
console.log(`Deployed generic verifier on 138: ${summary.deployedGenericVerifierAddress}`);
}
for (const row of rows) {
const parts = [
`${row.chainId} ${row.chainName}`,
`${row.familyKey}`,
`${row.canonicalSymbol}->${row.mirroredSymbol}`,
`selector=${row.destinationChainSelector || 'unset'}`,
`canonical=${row.canonicalCodeLive === true ? 'live' : row.canonicalCodeLive === false ? 'missing' : 'unknown'}`,
`mirror=${row.mirroredCodeLive === true ? 'live' : row.mirroredCodeLive === false ? 'missing' : 'unknown'}`,
`l1=${row.l1BridgeAddress ? 'set' : 'unset'}`,
`l2=${row.l2BridgeAddress ? 'set' : 'unset'}`,
`verifier=${row.reserveVerifierAddress ? 'set' : 'unset'}`,
`l1cap=${row.l1BridgeCapability}`,
`l1dest=${row.destinationConfiguredOnL1 === true ? 'wired' : row.destinationConfiguredOnL1 === false ? 'missing' : 'unknown'}`,
];
console.log(`- ${parts.join(' | ')}`);
if (row.destinationReceiverBridgeOnL1) {
console.log(
` l1 destination receiver: ${row.destinationReceiverBridgeOnL1}${row.destinationReceiverMatchesRuntimeL2 ? ' (matches runtime L2)' : ' (differs from runtime L2)'}`
);
}
if (row.l1BridgeReadableViews.length > 0) {
console.log(` l1 readable views: ${row.l1BridgeReadableViews.join(', ')}`);
}
const failedProbeViews = Object.entries(row.l1BridgeProbeErrors)
.filter(([, error]) => typeof error === 'string' && error)
.map(([label]) => label);
if (failedProbeViews.length > 0) {
console.log(` l1 probe gaps: ${failedProbeViews.join(', ')}`);
}
if (row.runtimeMissingRequirements.length > 0) {
console.log(` missing: ${row.runtimeMissingRequirements.join(', ')}`);
}
for (const action of row.actions.slice(0, 3)) {
console.log(` next ${action.type}: ${action.command}`);
}
}
NODE