691 lines
21 KiB
Bash
691 lines
21 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Audit deployer balances plus deployer access posture for Chain 138 c* and public-chain cW* tokens.
|
||
|
|
#
|
||
|
|
# Exports:
|
||
|
|
# - balances JSON
|
||
|
|
# - balances CSV
|
||
|
|
# - access JSON
|
||
|
|
# - access CSV
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# bash scripts/verify/audit-deployer-token-access.sh
|
||
|
|
# bash scripts/verify/audit-deployer-token-access.sh --output-dir reports/deployer-token-audit
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||
|
|
export PROJECT_ROOT
|
||
|
|
|
||
|
|
OUTPUT_DIR="${PROJECT_ROOT}/reports/deployer-token-audit"
|
||
|
|
|
||
|
|
while [[ $# -gt 0 ]]; do
|
||
|
|
case "$1" in
|
||
|
|
--output-dir)
|
||
|
|
[[ $# -ge 2 ]] || { echo "Missing value for --output-dir" >&2; exit 2; }
|
||
|
|
OUTPUT_DIR="$2"
|
||
|
|
shift 2
|
||
|
|
;;
|
||
|
|
*)
|
||
|
|
echo "Unknown argument: $1" >&2
|
||
|
|
exit 2
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh"
|
||
|
|
|
||
|
|
command -v cast >/dev/null 2>&1 || { echo "Missing required command: cast" >&2; exit 1; }
|
||
|
|
command -v node >/dev/null 2>&1 || { echo "Missing required command: node" >&2; exit 1; }
|
||
|
|
|
||
|
|
if [[ -n "${DEPLOYER_ADDRESS:-}" ]]; then
|
||
|
|
AUDIT_DEPLOYER_ADDRESS="$DEPLOYER_ADDRESS"
|
||
|
|
else
|
||
|
|
[[ -n "${PRIVATE_KEY:-}" ]] || {
|
||
|
|
echo "Missing PRIVATE_KEY or DEPLOYER_ADDRESS in environment" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
AUDIT_DEPLOYER_ADDRESS="$(cast wallet address "$PRIVATE_KEY")"
|
||
|
|
fi
|
||
|
|
|
||
|
|
[[ -n "$AUDIT_DEPLOYER_ADDRESS" ]] || {
|
||
|
|
echo "Failed to derive deployer address" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
mkdir -p "$OUTPUT_DIR"
|
||
|
|
|
||
|
|
export OUTPUT_DIR
|
||
|
|
export AUDIT_DEPLOYER_ADDRESS
|
||
|
|
export AUDIT_TIMESTAMP_UTC="$(date -u +%Y%m%dT%H%M%SZ)"
|
||
|
|
|
||
|
|
node <<'NODE'
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
const util = require('util');
|
||
|
|
const { execFile } = require('child_process');
|
||
|
|
|
||
|
|
const execFileAsync = util.promisify(execFile);
|
||
|
|
|
||
|
|
const projectRoot = process.env.PROJECT_ROOT;
|
||
|
|
const outputDir = path.resolve(process.env.OUTPUT_DIR);
|
||
|
|
const generatedAt = process.env.AUDIT_TIMESTAMP_UTC;
|
||
|
|
const deployerAddress = process.env.AUDIT_DEPLOYER_ADDRESS;
|
||
|
|
|
||
|
|
const roleHashes = {
|
||
|
|
defaultAdmin: '0x' + '0'.repeat(64),
|
||
|
|
minter: null,
|
||
|
|
burner: null,
|
||
|
|
pauser: null,
|
||
|
|
bridge: null,
|
||
|
|
governance: null,
|
||
|
|
jurisdictionAdmin: null,
|
||
|
|
regulator: null,
|
||
|
|
supervisor: null,
|
||
|
|
emergencyAdmin: null,
|
||
|
|
supplyAdmin: null,
|
||
|
|
metadataAdmin: null,
|
||
|
|
};
|
||
|
|
|
||
|
|
const publicChainMeta = {
|
||
|
|
'1': {
|
||
|
|
chainKey: 'MAINNET',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_ETHEREUM',
|
||
|
|
rpcEnvCandidates: ['ETH_MAINNET_RPC_URL', 'ETHEREUM_MAINNET_RPC'],
|
||
|
|
fallbackRpc: 'https://eth.llamarpc.com',
|
||
|
|
fallbackLabel: 'fallback:eth.llamarpc.com',
|
||
|
|
spenderEnv: 'CW_BRIDGE_MAINNET',
|
||
|
|
},
|
||
|
|
'10': {
|
||
|
|
chainKey: 'OPTIMISM',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_OPTIMISM',
|
||
|
|
rpcEnvCandidates: ['OPTIMISM_MAINNET_RPC', 'OPTIMISM_RPC_URL'],
|
||
|
|
fallbackRpc: 'https://mainnet.optimism.io',
|
||
|
|
fallbackLabel: 'fallback:mainnet.optimism.io',
|
||
|
|
spenderEnv: 'CW_BRIDGE_OPTIMISM',
|
||
|
|
},
|
||
|
|
'25': {
|
||
|
|
chainKey: 'CRONOS',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_CRONOS',
|
||
|
|
rpcEnvCandidates: ['CRONOS_CW_VERIFY_RPC_URL', 'CRONOS_RPC_URL', 'CRONOS_RPC'],
|
||
|
|
fallbackRpc: 'https://evm.cronos.org',
|
||
|
|
fallbackLabel: 'fallback:evm.cronos.org',
|
||
|
|
spenderEnv: 'CW_BRIDGE_CRONOS',
|
||
|
|
},
|
||
|
|
'56': {
|
||
|
|
chainKey: 'BSC',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_BSC',
|
||
|
|
rpcEnvCandidates: ['BSC_RPC_URL', 'BSC_MAINNET_RPC'],
|
||
|
|
fallbackRpc: 'https://bsc-dataseed.binance.org',
|
||
|
|
fallbackLabel: 'fallback:bsc-dataseed.binance.org',
|
||
|
|
spenderEnv: 'CW_BRIDGE_BSC',
|
||
|
|
},
|
||
|
|
'100': {
|
||
|
|
chainKey: 'GNOSIS',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_GNOSIS',
|
||
|
|
rpcEnvCandidates: ['GNOSIS_MAINNET_RPC', 'GNOSIS_RPC_URL', 'GNOSIS_RPC'],
|
||
|
|
fallbackRpc: 'https://rpc.gnosischain.com',
|
||
|
|
fallbackLabel: 'fallback:rpc.gnosischain.com',
|
||
|
|
spenderEnv: 'CW_BRIDGE_GNOSIS',
|
||
|
|
},
|
||
|
|
'137': {
|
||
|
|
chainKey: 'POLYGON',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_POLYGON',
|
||
|
|
rpcEnvCandidates: ['POLYGON_MAINNET_RPC', 'POLYGON_RPC_URL'],
|
||
|
|
fallbackRpc: 'https://polygon-rpc.com',
|
||
|
|
fallbackLabel: 'fallback:polygon-rpc.com',
|
||
|
|
spenderEnv: 'CW_BRIDGE_POLYGON',
|
||
|
|
},
|
||
|
|
'42220': {
|
||
|
|
chainKey: 'CELO',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_CELO',
|
||
|
|
rpcEnvCandidates: ['CELO_RPC', 'CELO_MAINNET_RPC'],
|
||
|
|
fallbackRpc: 'https://forno.celo.org',
|
||
|
|
fallbackLabel: 'fallback:forno.celo.org',
|
||
|
|
spenderEnv: 'CW_BRIDGE_CELO',
|
||
|
|
},
|
||
|
|
'43114': {
|
||
|
|
chainKey: 'AVALANCHE',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_AVALANCHE',
|
||
|
|
rpcEnvCandidates: ['AVALANCHE_RPC_URL', 'AVALANCHE_MAINNET_RPC', 'AVALANCHE_RPC'],
|
||
|
|
fallbackRpc: 'https://api.avax.network/ext/bc/C/rpc',
|
||
|
|
fallbackLabel: 'fallback:api.avax.network/ext/bc/C/rpc',
|
||
|
|
spenderEnv: 'CW_BRIDGE_AVALANCHE',
|
||
|
|
},
|
||
|
|
'8453': {
|
||
|
|
chainKey: 'BASE',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_BASE',
|
||
|
|
rpcEnvCandidates: ['BASE_MAINNET_RPC', 'BASE_RPC_URL'],
|
||
|
|
fallbackRpc: 'https://mainnet.base.org',
|
||
|
|
fallbackLabel: 'fallback:mainnet.base.org',
|
||
|
|
spenderEnv: 'CW_BRIDGE_BASE',
|
||
|
|
},
|
||
|
|
'42161': {
|
||
|
|
chainKey: 'ARBITRUM',
|
||
|
|
walletEnv: 'DEST_FUND_WALLET_ARBITRUM',
|
||
|
|
rpcEnvCandidates: ['ARBITRUM_MAINNET_RPC', 'ARBITRUM_RPC_URL'],
|
||
|
|
fallbackRpc: 'https://arb1.arbitrum.io/rpc',
|
||
|
|
fallbackLabel: 'fallback:arb1.arbitrum.io/rpc',
|
||
|
|
spenderEnv: 'CW_BRIDGE_ARBITRUM',
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
function quoteCsv(value) {
|
||
|
|
const stringValue = value == null ? '' : String(value);
|
||
|
|
return `"${stringValue.replace(/"/g, '""')}"`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatUnits(rawValue, decimals) {
|
||
|
|
if (rawValue == null) return null;
|
||
|
|
const raw = BigInt(rawValue);
|
||
|
|
const base = 10n ** BigInt(decimals);
|
||
|
|
const whole = raw / base;
|
||
|
|
const fraction = raw % base;
|
||
|
|
if (fraction === 0n) return whole.toString();
|
||
|
|
const padded = fraction.toString().padStart(decimals, '0').replace(/0+$/, '');
|
||
|
|
return `${whole.toString()}.${padded}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizeAddress(value) {
|
||
|
|
if (!value) return null;
|
||
|
|
const trimmed = String(value).trim();
|
||
|
|
if (!trimmed) return null;
|
||
|
|
return trimmed;
|
||
|
|
}
|
||
|
|
|
||
|
|
function stripQuotes(value) {
|
||
|
|
if (value == null) return null;
|
||
|
|
const trimmed = String(value).trim();
|
||
|
|
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||
|
|
return trimmed.slice(1, -1);
|
||
|
|
}
|
||
|
|
return trimmed;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseBigIntOutput(value) {
|
||
|
|
if (value == null) return null;
|
||
|
|
const trimmed = String(value).trim();
|
||
|
|
if (!trimmed) return null;
|
||
|
|
const token = trimmed.split(/\s+/)[0];
|
||
|
|
return BigInt(token);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function runCast(args, { allowFailure = false } = {}) {
|
||
|
|
try {
|
||
|
|
const { stdout } = await execFileAsync('cast', args, {
|
||
|
|
encoding: 'utf8',
|
||
|
|
timeout: 20000,
|
||
|
|
maxBuffer: 1024 * 1024,
|
||
|
|
});
|
||
|
|
return stdout.trim();
|
||
|
|
} catch (error) {
|
||
|
|
if (allowFailure) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const stderr = error.stderr ? `\n${String(error.stderr).trim()}` : '';
|
||
|
|
throw new Error(`cast ${args.join(' ')} failed${stderr}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function buildRoleHashes() {
|
||
|
|
const keys = Object.keys(roleHashes).filter((key) => key !== 'defaultAdmin');
|
||
|
|
await Promise.all(keys.map(async (key) => {
|
||
|
|
const labelMap = {
|
||
|
|
minter: 'MINTER_ROLE',
|
||
|
|
burner: 'BURNER_ROLE',
|
||
|
|
pauser: 'PAUSER_ROLE',
|
||
|
|
bridge: 'BRIDGE_ROLE',
|
||
|
|
governance: 'GOVERNANCE_ROLE',
|
||
|
|
jurisdictionAdmin: 'JURISDICTION_ADMIN_ROLE',
|
||
|
|
regulator: 'REGULATOR_ROLE',
|
||
|
|
supervisor: 'SUPERVISOR_ROLE',
|
||
|
|
emergencyAdmin: 'EMERGENCY_ADMIN_ROLE',
|
||
|
|
supplyAdmin: 'SUPPLY_ADMIN_ROLE',
|
||
|
|
metadataAdmin: 'METADATA_ADMIN_ROLE',
|
||
|
|
};
|
||
|
|
roleHashes[key] = await runCast(['keccak', labelMap[key]]);
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolvePublicChain(chainId, chainName) {
|
||
|
|
const meta = publicChainMeta[chainId];
|
||
|
|
if (!meta) {
|
||
|
|
return {
|
||
|
|
chainId: Number(chainId),
|
||
|
|
chainName,
|
||
|
|
walletAddress: deployerAddress,
|
||
|
|
walletSource: 'AUDIT_DEPLOYER_ADDRESS',
|
||
|
|
rpcUrl: null,
|
||
|
|
rpcSource: 'missing',
|
||
|
|
spenderAddress: null,
|
||
|
|
spenderLabel: null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
let rpcUrl = null;
|
||
|
|
let rpcSource = 'missing';
|
||
|
|
for (const key of meta.rpcEnvCandidates) {
|
||
|
|
if (process.env[key]) {
|
||
|
|
rpcUrl = process.env[key];
|
||
|
|
rpcSource = key;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!rpcUrl && meta.fallbackRpc) {
|
||
|
|
rpcUrl = meta.fallbackRpc;
|
||
|
|
rpcSource = meta.fallbackLabel;
|
||
|
|
}
|
||
|
|
|
||
|
|
const walletAddress = normalizeAddress(process.env[meta.walletEnv]) || deployerAddress;
|
||
|
|
const walletSource = process.env[meta.walletEnv] ? meta.walletEnv : 'AUDIT_DEPLOYER_ADDRESS';
|
||
|
|
const spenderAddress = normalizeAddress(process.env[meta.spenderEnv]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
chainId: Number(chainId),
|
||
|
|
chainKey: meta.chainKey,
|
||
|
|
chainName,
|
||
|
|
walletAddress,
|
||
|
|
walletSource,
|
||
|
|
rpcUrl,
|
||
|
|
rpcSource,
|
||
|
|
spenderAddress,
|
||
|
|
spenderLabel: meta.spenderEnv,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function callUint(contract, rpcUrl, signature, extraArgs = []) {
|
||
|
|
const output = await runCast(['call', contract, signature, ...extraArgs, '--rpc-url', rpcUrl], { allowFailure: true });
|
||
|
|
if (output == null || output === '') return null;
|
||
|
|
return parseBigIntOutput(output);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function callBool(contract, rpcUrl, signature, extraArgs = []) {
|
||
|
|
const output = await runCast(['call', contract, signature, ...extraArgs, '--rpc-url', rpcUrl], { allowFailure: true });
|
||
|
|
if (output == null || output === '') return null;
|
||
|
|
if (output === 'true') return true;
|
||
|
|
if (output === 'false') return false;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function callAddress(contract, rpcUrl, signature, extraArgs = []) {
|
||
|
|
const output = await runCast(['call', contract, signature, ...extraArgs, '--rpc-url', rpcUrl], { allowFailure: true });
|
||
|
|
if (output == null || output === '') return null;
|
||
|
|
const address = normalizeAddress(stripQuotes(output));
|
||
|
|
return address && /^0x[a-fA-F0-9]{40}$/.test(address) ? address : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function callString(contract, rpcUrl, signature, extraArgs = []) {
|
||
|
|
const output = await runCast(['call', contract, signature, ...extraArgs, '--rpc-url', rpcUrl], { allowFailure: true });
|
||
|
|
if (output == null || output === '') return null;
|
||
|
|
return stripQuotes(output);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function mapLimit(items, limit, iterator) {
|
||
|
|
const results = new Array(items.length);
|
||
|
|
let nextIndex = 0;
|
||
|
|
|
||
|
|
async function worker() {
|
||
|
|
for (;;) {
|
||
|
|
const current = nextIndex;
|
||
|
|
nextIndex += 1;
|
||
|
|
if (current >= items.length) return;
|
||
|
|
results[current] = await iterator(items[current], current);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const workerCount = Math.min(limit, items.length || 1);
|
||
|
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
|
||
|
|
function roleColumns(row) {
|
||
|
|
return {
|
||
|
|
default_admin: row.roles.defaultAdmin,
|
||
|
|
minter: row.roles.minter,
|
||
|
|
burner: row.roles.burner,
|
||
|
|
pauser: row.roles.pauser,
|
||
|
|
bridge: row.roles.bridge,
|
||
|
|
governance: row.roles.governance,
|
||
|
|
jurisdiction_admin: row.roles.jurisdictionAdmin,
|
||
|
|
regulator: row.roles.regulator,
|
||
|
|
supervisor: row.roles.supervisor,
|
||
|
|
emergency_admin: row.roles.emergencyAdmin,
|
||
|
|
supply_admin: row.roles.supplyAdmin,
|
||
|
|
metadata_admin: row.roles.metadataAdmin,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function auditToken(token) {
|
||
|
|
const rpcUrl = token.rpcUrl;
|
||
|
|
const balanceRowBase = {
|
||
|
|
category: token.category,
|
||
|
|
chain_id: token.chainId,
|
||
|
|
chain_name: token.chainName,
|
||
|
|
token_key: token.tokenKey,
|
||
|
|
token_address: token.tokenAddress,
|
||
|
|
wallet_address: token.walletAddress,
|
||
|
|
wallet_source: token.walletSource,
|
||
|
|
rpc_source: token.rpcSource,
|
||
|
|
};
|
||
|
|
|
||
|
|
const accessRowBase = {
|
||
|
|
category: token.category,
|
||
|
|
chain_id: token.chainId,
|
||
|
|
chain_name: token.chainName,
|
||
|
|
token_key: token.tokenKey,
|
||
|
|
token_address: token.tokenAddress,
|
||
|
|
wallet_address: token.walletAddress,
|
||
|
|
wallet_source: token.walletSource,
|
||
|
|
rpc_source: token.rpcSource,
|
||
|
|
spender_label: token.spenderLabel,
|
||
|
|
spender_address: token.spenderAddress,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!rpcUrl) {
|
||
|
|
return {
|
||
|
|
balance: {
|
||
|
|
...balanceRowBase,
|
||
|
|
decimals: null,
|
||
|
|
balance_raw: null,
|
||
|
|
balance_formatted: null,
|
||
|
|
query_status: 'missing_rpc',
|
||
|
|
},
|
||
|
|
access: {
|
||
|
|
...accessRowBase,
|
||
|
|
owner: null,
|
||
|
|
owner_matches_wallet: null,
|
||
|
|
allowance_raw: null,
|
||
|
|
allowance_formatted: null,
|
||
|
|
query_status: 'missing_rpc',
|
||
|
|
roles: Object.fromEntries(Object.keys(roleHashes).map((key) => [key, null])),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const decimalsPromise = callUint(token.tokenAddress, rpcUrl, 'decimals()(uint8)');
|
||
|
|
const balancePromise = callUint(token.tokenAddress, rpcUrl, 'balanceOf(address)(uint256)', [token.walletAddress]);
|
||
|
|
const ownerPromise = callAddress(token.tokenAddress, rpcUrl, 'owner()(address)');
|
||
|
|
const symbolPromise = callString(token.tokenAddress, rpcUrl, 'symbol()(string)');
|
||
|
|
|
||
|
|
const allowancePromise =
|
||
|
|
token.spenderAddress
|
||
|
|
? callUint(token.tokenAddress, rpcUrl, 'allowance(address,address)(uint256)', [token.walletAddress, token.spenderAddress])
|
||
|
|
: Promise.resolve(null);
|
||
|
|
|
||
|
|
const roleNames = Object.keys(roleHashes);
|
||
|
|
const rolePairs = await mapLimit(roleNames, 4, async (roleName) => {
|
||
|
|
const roleValue = await callBool(
|
||
|
|
token.tokenAddress,
|
||
|
|
rpcUrl,
|
||
|
|
'hasRole(bytes32,address)(bool)',
|
||
|
|
[roleHashes[roleName], token.walletAddress],
|
||
|
|
);
|
||
|
|
return [roleName, roleValue];
|
||
|
|
});
|
||
|
|
|
||
|
|
const roles = Object.fromEntries(rolePairs);
|
||
|
|
const [decimalsRaw, balanceRaw, owner, symbol, allowanceRaw] = await Promise.all([
|
||
|
|
decimalsPromise,
|
||
|
|
balancePromise,
|
||
|
|
ownerPromise,
|
||
|
|
symbolPromise,
|
||
|
|
allowancePromise,
|
||
|
|
]);
|
||
|
|
|
||
|
|
const decimals = decimalsRaw == null ? null : Number(decimalsRaw);
|
||
|
|
const effectiveDecimals = decimals == null ? 18 : decimals;
|
||
|
|
const balanceFormatted = balanceRaw == null ? null : formatUnits(balanceRaw, effectiveDecimals);
|
||
|
|
const allowanceFormatted = allowanceRaw == null ? null : formatUnits(allowanceRaw, effectiveDecimals);
|
||
|
|
const ownerMatchesWallet = owner == null ? null : owner.toLowerCase() === token.walletAddress.toLowerCase();
|
||
|
|
|
||
|
|
return {
|
||
|
|
balance: {
|
||
|
|
...balanceRowBase,
|
||
|
|
token_symbol: symbol || token.tokenKey,
|
||
|
|
decimals,
|
||
|
|
balance_raw: balanceRaw == null ? null : balanceRaw.toString(),
|
||
|
|
balance_formatted: balanceFormatted,
|
||
|
|
query_status: balanceRaw == null ? 'query_failed' : 'ok',
|
||
|
|
},
|
||
|
|
access: {
|
||
|
|
...accessRowBase,
|
||
|
|
token_symbol: symbol || token.tokenKey,
|
||
|
|
owner,
|
||
|
|
owner_matches_wallet: ownerMatchesWallet,
|
||
|
|
allowance_raw: allowanceRaw == null ? null : allowanceRaw.toString(),
|
||
|
|
allowance_formatted: allowanceFormatted,
|
||
|
|
query_status: 'ok',
|
||
|
|
roles,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function balanceCsv(rows) {
|
||
|
|
const header = [
|
||
|
|
'category',
|
||
|
|
'chain_id',
|
||
|
|
'chain_name',
|
||
|
|
'token_key',
|
||
|
|
'token_symbol',
|
||
|
|
'token_address',
|
||
|
|
'wallet_address',
|
||
|
|
'wallet_source',
|
||
|
|
'rpc_source',
|
||
|
|
'decimals',
|
||
|
|
'balance_raw',
|
||
|
|
'balance_formatted',
|
||
|
|
'query_status',
|
||
|
|
];
|
||
|
|
const lines = [header.join(',')];
|
||
|
|
for (const row of rows) {
|
||
|
|
lines.push([
|
||
|
|
row.category,
|
||
|
|
row.chain_id,
|
||
|
|
row.chain_name,
|
||
|
|
row.token_key,
|
||
|
|
row.token_symbol,
|
||
|
|
row.token_address,
|
||
|
|
row.wallet_address,
|
||
|
|
row.wallet_source,
|
||
|
|
row.rpc_source,
|
||
|
|
row.decimals,
|
||
|
|
row.balance_raw,
|
||
|
|
row.balance_formatted,
|
||
|
|
row.query_status,
|
||
|
|
].map(quoteCsv).join(','));
|
||
|
|
}
|
||
|
|
return `${lines.join('\n')}\n`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function accessCsv(rows) {
|
||
|
|
const header = [
|
||
|
|
'category',
|
||
|
|
'chain_id',
|
||
|
|
'chain_name',
|
||
|
|
'token_key',
|
||
|
|
'token_symbol',
|
||
|
|
'token_address',
|
||
|
|
'wallet_address',
|
||
|
|
'wallet_source',
|
||
|
|
'rpc_source',
|
||
|
|
'owner',
|
||
|
|
'owner_matches_wallet',
|
||
|
|
'spender_label',
|
||
|
|
'spender_address',
|
||
|
|
'allowance_raw',
|
||
|
|
'allowance_formatted',
|
||
|
|
'default_admin',
|
||
|
|
'minter',
|
||
|
|
'burner',
|
||
|
|
'pauser',
|
||
|
|
'bridge',
|
||
|
|
'governance',
|
||
|
|
'jurisdiction_admin',
|
||
|
|
'regulator',
|
||
|
|
'supervisor',
|
||
|
|
'emergency_admin',
|
||
|
|
'supply_admin',
|
||
|
|
'metadata_admin',
|
||
|
|
'query_status',
|
||
|
|
];
|
||
|
|
const lines = [header.join(',')];
|
||
|
|
for (const row of rows) {
|
||
|
|
const roles = roleColumns(row);
|
||
|
|
lines.push([
|
||
|
|
row.category,
|
||
|
|
row.chain_id,
|
||
|
|
row.chain_name,
|
||
|
|
row.token_key,
|
||
|
|
row.token_symbol,
|
||
|
|
row.token_address,
|
||
|
|
row.wallet_address,
|
||
|
|
row.wallet_source,
|
||
|
|
row.rpc_source,
|
||
|
|
row.owner,
|
||
|
|
row.owner_matches_wallet,
|
||
|
|
row.spender_label,
|
||
|
|
row.spender_address,
|
||
|
|
row.allowance_raw,
|
||
|
|
row.allowance_formatted,
|
||
|
|
roles.default_admin,
|
||
|
|
roles.minter,
|
||
|
|
roles.burner,
|
||
|
|
roles.pauser,
|
||
|
|
roles.bridge,
|
||
|
|
roles.governance,
|
||
|
|
roles.jurisdiction_admin,
|
||
|
|
roles.regulator,
|
||
|
|
roles.supervisor,
|
||
|
|
roles.emergency_admin,
|
||
|
|
roles.supply_admin,
|
||
|
|
roles.metadata_admin,
|
||
|
|
row.query_status,
|
||
|
|
].map(quoteCsv).join(','));
|
||
|
|
}
|
||
|
|
return `${lines.join('\n')}\n`;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
await buildRoleHashes();
|
||
|
|
|
||
|
|
const chain138Registry = JSON.parse(
|
||
|
|
fs.readFileSync(path.join(projectRoot, 'config/smart-contracts-master.json'), 'utf8'),
|
||
|
|
);
|
||
|
|
const deploymentStatus = JSON.parse(
|
||
|
|
fs.readFileSync(path.join(projectRoot, 'cross-chain-pmm-lps/config/deployment-status.json'), 'utf8'),
|
||
|
|
);
|
||
|
|
|
||
|
|
const chain138Contracts = (((chain138Registry || {}).chains || {})['138'] || {}).contracts || {};
|
||
|
|
const cStarTokens = Object.entries(chain138Contracts)
|
||
|
|
.filter(([name, address]) => name.startsWith('c') && !name.includes('_Pool_') && typeof address === 'string')
|
||
|
|
.map(([tokenKey, tokenAddress]) => ({
|
||
|
|
category: 'cstar',
|
||
|
|
chainId: 138,
|
||
|
|
chainName: 'Chain 138',
|
||
|
|
tokenKey,
|
||
|
|
tokenAddress,
|
||
|
|
walletAddress: deployerAddress,
|
||
|
|
walletSource: 'AUDIT_DEPLOYER_ADDRESS',
|
||
|
|
rpcUrl: process.env.RPC_URL_138 || process.env.CHAIN138_RPC_URL || process.env.RPC_URL || null,
|
||
|
|
rpcSource: process.env.RPC_URL_138 ? 'RPC_URL_138'
|
||
|
|
: process.env.CHAIN138_RPC_URL ? 'CHAIN138_RPC_URL'
|
||
|
|
: process.env.RPC_URL ? 'RPC_URL'
|
||
|
|
: 'missing',
|
||
|
|
spenderAddress: null,
|
||
|
|
spenderLabel: null,
|
||
|
|
}));
|
||
|
|
|
||
|
|
const cWTokens = [];
|
||
|
|
for (const [chainId, chainInfo] of Object.entries(deploymentStatus.chains || {})) {
|
||
|
|
const cwTokens = chainInfo.cwTokens || {};
|
||
|
|
const resolved = resolvePublicChain(chainId, chainInfo.name);
|
||
|
|
for (const [tokenKey, tokenAddress] of Object.entries(cwTokens)) {
|
||
|
|
cWTokens.push({
|
||
|
|
category: 'cw',
|
||
|
|
chainId: Number(chainId),
|
||
|
|
chainName: chainInfo.name,
|
||
|
|
tokenKey,
|
||
|
|
tokenAddress,
|
||
|
|
walletAddress: resolved.walletAddress,
|
||
|
|
walletSource: resolved.walletSource,
|
||
|
|
rpcUrl: resolved.rpcUrl,
|
||
|
|
rpcSource: resolved.rpcSource,
|
||
|
|
spenderAddress: resolved.spenderAddress,
|
||
|
|
spenderLabel: resolved.spenderLabel,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const audited = await mapLimit([...cStarTokens, ...cWTokens], 6, auditToken);
|
||
|
|
const balanceRows = audited.map((row) => row.balance);
|
||
|
|
const accessRows = audited.map((row) => row.access);
|
||
|
|
|
||
|
|
balanceRows.sort((a, b) => (a.chain_id - b.chain_id) || a.token_key.localeCompare(b.token_key));
|
||
|
|
accessRows.sort((a, b) => (a.chain_id - b.chain_id) || a.token_key.localeCompare(b.token_key));
|
||
|
|
|
||
|
|
const balancesJson = {
|
||
|
|
generatedAt,
|
||
|
|
deployerAddress,
|
||
|
|
summary: {
|
||
|
|
tokensChecked: balanceRows.length,
|
||
|
|
cStarTokensChecked: balanceRows.filter((row) => row.category === 'cstar').length,
|
||
|
|
cWTokensChecked: balanceRows.filter((row) => row.category === 'cw').length,
|
||
|
|
nonZeroBalances: balanceRows.filter((row) => row.balance_raw != null && row.balance_raw !== '0').length,
|
||
|
|
queryFailures: balanceRows.filter((row) => row.query_status !== 'ok').length,
|
||
|
|
},
|
||
|
|
balances: balanceRows,
|
||
|
|
};
|
||
|
|
|
||
|
|
const accessJson = {
|
||
|
|
generatedAt,
|
||
|
|
deployerAddress,
|
||
|
|
summary: {
|
||
|
|
tokensChecked: accessRows.length,
|
||
|
|
tokensWithOwnerFunction: accessRows.filter((row) => row.owner != null).length,
|
||
|
|
tokensWithBridgeAllowanceChecks: accessRows.filter((row) => row.spender_address != null).length,
|
||
|
|
nonZeroAllowances: accessRows.filter((row) => row.allowance_raw != null && row.allowance_raw !== '0').length,
|
||
|
|
deployerDefaultAdminCount: accessRows.filter((row) => row.roles.defaultAdmin === true).length,
|
||
|
|
deployerMinterCount: accessRows.filter((row) => row.roles.minter === true).length,
|
||
|
|
deployerBurnerCount: accessRows.filter((row) => row.roles.burner === true).length,
|
||
|
|
},
|
||
|
|
access: accessRows,
|
||
|
|
};
|
||
|
|
|
||
|
|
const balanceJsonPath = path.join(outputDir, `deployer-token-balances-${generatedAt}.json`);
|
||
|
|
const balanceCsvPath = path.join(outputDir, `deployer-token-balances-${generatedAt}.csv`);
|
||
|
|
const accessJsonPath = path.join(outputDir, `deployer-token-access-${generatedAt}.json`);
|
||
|
|
const accessCsvPath = path.join(outputDir, `deployer-token-access-${generatedAt}.csv`);
|
||
|
|
|
||
|
|
fs.writeFileSync(balanceJsonPath, JSON.stringify(balancesJson, null, 2));
|
||
|
|
fs.writeFileSync(balanceCsvPath, balanceCsv(balanceRows));
|
||
|
|
fs.writeFileSync(accessJsonPath, JSON.stringify(accessJson, null, 2));
|
||
|
|
fs.writeFileSync(accessCsvPath, accessCsv(accessRows));
|
||
|
|
|
||
|
|
console.log(JSON.stringify({
|
||
|
|
generatedAt,
|
||
|
|
deployerAddress,
|
||
|
|
outputDir,
|
||
|
|
files: {
|
||
|
|
balancesJson: balanceJsonPath,
|
||
|
|
balancesCsv: balanceCsvPath,
|
||
|
|
accessJson: accessJsonPath,
|
||
|
|
accessCsv: accessCsvPath,
|
||
|
|
},
|
||
|
|
summary: {
|
||
|
|
cStarTokensChecked: balancesJson.summary.cStarTokensChecked,
|
||
|
|
cWTokensChecked: balancesJson.summary.cWTokensChecked,
|
||
|
|
nonZeroBalances: balancesJson.summary.nonZeroBalances,
|
||
|
|
nonZeroAllowances: accessJson.summary.nonZeroAllowances,
|
||
|
|
deployerDefaultAdminCount: accessJson.summary.deployerDefaultAdminCount,
|
||
|
|
deployerMinterCount: accessJson.summary.deployerMinterCount,
|
||
|
|
deployerBurnerCount: accessJson.summary.deployerBurnerCount,
|
||
|
|
},
|
||
|
|
}, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((error) => {
|
||
|
|
console.error(error instanceof Error ? error.message : String(error));
|
||
|
|
process.exit(1);
|
||
|
|
});
|
||
|
|
NODE
|