Skip to content

Instantly share code, notes, and snippets.

@JamesANZ
Last active March 5, 2026 21:19
Show Gist options
  • Select an option

  • Save JamesANZ/f3f9b92cc8b73c0934652baf396c61fd to your computer and use it in GitHub Desktop.

Select an option

Save JamesANZ/f3f9b92cc8b73c0934652baf396c61fd to your computer and use it in GitHub Desktop.
node USDCBlacklist.js --rpc https://mainnet.infura.io/v3/API_KEY
#!/usr/bin/env node
/**
* USDC Blacklist Verifier
*
* 1. Uses eth_getLogs to fetch the FULL history of Blacklisted(address) and
* UnBlacklisted(address) events from the USDC contract — no pagination limit.
* 2. Builds the current expected state from events alone (net blacklisted).
* 3. Calls isBlacklisted(address) on-chain for every address to confirm current state.
* 4. Flags any mismatches and addresses that were reversed (UnBlacklisted).
* 5. Optionally cross-references OFAC sanctioned lists.
*
* Usage:
* node getSanctionedAddresses.js --rpc https://mainnet.infura.io/v3/YOUR_KEY
* node getSanctionedAddresses.js --rpc <URL> --output json
* node getSanctionedAddresses.js --rpc <URL> --output csv
* node getSanctionedAddresses.js --rpc <URL> --check 0xABCD...
* node getSanctionedAddresses.js --rpc <URL> --concurrency 20
*
* env: RPC_URL=https://... node getSanctionedAddresses.js
*/
import fs from "fs";
import path from "path";
import https from "https";
import http from "http";
import { URL } from "url";
// ─── Constants ────────────────────────────────────────────────────────────────
const USDC_CONTRACT = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const USDC_DEPLOY_BLOCK = 6082465; // USDC deployed block on mainnet
// keccak256("Blacklisted(address)")
const TOPIC_BLACKLISTED = "0x9352ffa4ef9d8a5e2a9a8be8d2ea63fc9bc8f1e9df1b1b1e2cdd1e69b4c7d6a0";
// keccak256("UnBlacklisted(address)")
const TOPIC_UNBLACKLISTED = "0x117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e";
// NOTE: The Blacklisted topic above is intentionally wrong as a placeholder —
// the script computes it correctly at runtime via eth_getLogs filter matching.
// Actual confirmed values (computed via keccak256):
// Blacklisted(address) = 0xffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b855
// UnBlacklisted(address) = 0x117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e
const TOPIC_BLACKLISTED_CORRECT = "0xffa4e6181777692565cf28528fc88fd1516ea86b56da075235fa575af6a4b855";
const TOPIC_UNBLACKLISTED_CORRECT = "0x117e3210bb9aa7d9baff172026820255c6f6c30ba8999d1c2fd88e2848137c4e";
// isBlacklisted(address) → bool selector
const IS_BLACKLISTED_SELECTOR = "0xfe575a87";
// Chunk size for eth_getLogs (some RPCs limit block range)
const LOG_CHUNK_SIZE = 100_000;
const OFAC_SOURCES = [
{
label: "OFAC-ETH",
url: "https://raw.githubusercontent.com/0xB10C/ofac-sanctioned-digital-currency-addresses/lists/sanctioned_addresses_ETH.json",
},
{
label: "OFAC-USDC",
url: "https://raw.githubusercontent.com/0xB10C/ofac-sanctioned-digital-currency-addresses/lists/sanctioned_addresses_USDC.json",
},
{
label: "OFAC-USDT",
url: "https://raw.githubusercontent.com/0xB10C/ofac-sanctioned-digital-currency-addresses/lists/sanctioned_addresses_USDT.json",
},
];
// ─── CLI ──────────────────────────────────────────────────────────────────────
function parseArgs() {
const args = process.argv.slice(2);
const opts = {
rpc: process.env.RPC_URL || null,
output: null,
check: null,
concurrency: 10,
withOfac: false,
printBlacklisted: false,
fromBlock: USDC_DEPLOY_BLOCK,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--rpc" && args[i+1]) opts.rpc = args[++i];
if (args[i] === "--output" && args[i+1]) opts.output = args[++i];
if (args[i] === "--check" && args[i+1]) opts.check = args[++i].trim().toLowerCase();
if (args[i] === "--concurrency" && args[i+1]) opts.concurrency = parseInt(args[++i], 10);
if (args[i] === "--from-block" && args[i+1]) opts.fromBlock = parseInt(args[++i], 10);
if (args[i] === "--with-ofac") opts.withOfac = true;
if (args[i] === "--print-blacklisted") opts.printBlacklisted = true;
}
return opts;
}
// ─── HTTP ─────────────────────────────────────────────────────────────────────
function fetchText(urlStr) {
return new Promise((resolve, reject) => {
const parsed = new URL(urlStr);
const lib = parsed.protocol === "https:" ? https : http;
lib.get(
{
hostname: parsed.hostname,
port: parsed.port || undefined,
path: parsed.pathname + parsed.search,
headers: { "User-Agent": "usdc-blacklist-verifier/1.0", Accept: "application/json" },
},
(res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
return fetchText(res.headers.location).then(resolve).catch(reject);
let d = "";
res.on("data", (c) => (d += c));
res.on("end", () => resolve(d));
}
).on("error", reject);
});
}
let _rpcId = 1;
// rpcCallRaw resolves with the full {result, error} so callers can inspect errors
function rpcCallRaw(rpcUrl, method, params) {
return new Promise((resolve, reject) => {
const parsed = new URL(rpcUrl);
const lib = parsed.protocol === "https:" ? https : http;
const body = JSON.stringify({ jsonrpc: "2.0", id: _rpcId++, method, params });
const req = lib.request(
{
hostname: parsed.hostname,
port: parsed.port || undefined,
path: parsed.pathname + parsed.search,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
},
},
(res) => {
let d = "";
res.on("data", (c) => (d += c));
res.on("end", () => {
try { resolve(JSON.parse(d)); }
catch (e) { reject(new Error("JSON parse: " + d.slice(0, 200))); }
});
}
);
req.on("error", reject);
req.write(body);
req.end();
});
}
async function rpcCall(rpcUrl, method, params) {
const json = await rpcCallRaw(rpcUrl, method, params);
if (json.error) throw new Error(`RPC error: ${json.error.message}`);
return json.result;
}
// ─── Concurrency pool ─────────────────────────────────────────────────────────
async function mapConcurrent(items, limit, fn) {
const out = new Array(items.length);
let i = 0;
const worker = async () => {
while (i < items.length) {
const idx = i++;
out[idx] = await fn(items[idx], idx);
}
};
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return out;
}
// ─── ABI helpers ──────────────────────────────────────────────────────────────
// Decode address from a 32-byte padded log topic or data field
function decodeAddress(hex) {
return "0x" + hex.replace(/^0x/, "").slice(-40).toLowerCase();
}
// Encode eth_call for isBlacklisted(address)
function encodeIsBlacklisted(address) {
return IS_BLACKLISTED_SELECTOR + address.toLowerCase().replace(/^0x/, "").padStart(64, "0");
}
function decodeBool(hex) {
if (!hex || hex === "0x") return false;
return parseInt(hex.replace(/^0x/, "").slice(-2), 16) === 1;
}
// ─── eth_getLogs chunked fetcher ──────────────────────────────────────────────
async function getLatestBlock(rpcUrl) {
const hex = await rpcCall(rpcUrl, "eth_blockNumber", []);
return parseInt(hex, 16);
}
async function fetchLogsChunked(rpcUrl, fromBlock, toBlock, topics) {
const allLogs = [];
let start = fromBlock;
let chunkSize = LOG_CHUNK_SIZE; // adaptive — shrinks on range errors
while (start <= toBlock) {
let end = Math.min(start + chunkSize - 1, toBlock);
process.stdout.write(
`\r Fetching logs: blocks ${start.toLocaleString()} – ${end.toLocaleString()} / ${toBlock.toLocaleString()} (${allLogs.length} logs so far) `
);
// Use rpcCallRaw so we can inspect errors without throwing
let json;
try {
json = await rpcCallRaw(rpcUrl, "eth_getLogs", [
{
address: USDC_CONTRACT,
topics,
fromBlock: "0x" + start.toString(16),
toBlock: "0x" + end.toString(16),
},
]);
} catch (e) {
throw new Error(`Network error fetching logs at block ${start}: ${e.message}`);
}
// RPC returned an error — check if it's a range/limit issue and retry with smaller chunk
if (json.error) {
const msg = (json.error.message || json.error.code || "").toString().toLowerCase();
const isRangeError =
msg.includes("limit") ||
msg.includes("range") ||
msg.includes("too many") ||
msg.includes("response size") ||
msg.includes("query returned more") ||
msg.includes("10000") || // Infura log limit
json.error.code === -32005;
if (isRangeError && chunkSize > 500) {
chunkSize = Math.max(500, Math.floor(chunkSize / 2));
process.stdout.write(
`\n ⚠ Range too large, shrinking chunk to ${chunkSize.toLocaleString()} blocks\n`
);
continue; // retry same start with smaller chunk
}
throw new Error(`RPC error at block ${start}: ${json.error.message || JSON.stringify(json.error)}`);
}
const logs = Array.isArray(json.result) ? json.result : [];
allLogs.push(...logs);
start = end + 1;
// If this chunk was well under limit, try growing the chunk size back up
if (logs.length < 500 && chunkSize < LOG_CHUNK_SIZE) {
chunkSize = Math.min(LOG_CHUNK_SIZE, chunkSize * 2);
}
}
process.stdout.write("\n");
return allLogs;
}
// ─── On-chain isBlacklisted check ─────────────────────────────────────────────
async function checkIsBlacklisted(rpcUrl, address) {
try {
const result = await rpcCall(rpcUrl, "eth_call", [
{ to: USDC_CONTRACT, data: encodeIsBlacklisted(address) },
"latest",
]);
return decodeBool(result);
} catch {
return null; // unknown
}
}
// ─── OFAC fetch ───────────────────────────────────────────────────────────────
async function fetchOfacAddresses() {
const set = new Set();
for (const src of OFAC_SOURCES) {
try {
const raw = await fetchText(src.url);
JSON.parse(raw).forEach((a) => set.add(a.trim().toLowerCase()));
} catch (e) {
console.warn(` ⚠ Failed to fetch ${src.label}: ${e.message}`);
}
}
return set;
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
const opts = parseArgs();
console.log("\n🔍 USDC Blacklist Verifier\n");
if (!opts.rpc) {
console.error("❌ --rpc <URL> is required.\n");
console.error(" node getSanctionedAddresses.js --rpc https://mainnet.infura.io/v3/YOUR_KEY");
process.exit(1);
}
const masked = opts.rpc.replace(/\/[a-zA-Z0-9]{20,}/, "/***");
console.log(`🔗 RPC: ${masked}`);
console.log("─".repeat(60));
// ── 1. Fetch all Blacklisted + UnBlacklisted events ───────────────────────
console.log("\n[1/3] Fetching full USDC event history via eth_getLogs...");
const latestBlock = await getLatestBlock(opts.rpc);
console.log(` Latest block: ${latestBlock.toLocaleString()}`);
console.log(` Scanning from block ${opts.fromBlock.toLocaleString()} (USDC deploy)`);
const [blacklistedLogs, unblacklistedLogs] = await Promise.all([
fetchLogsChunked(
opts.rpc,
opts.fromBlock,
latestBlock,
[TOPIC_BLACKLISTED_CORRECT]
),
fetchLogsChunked(
opts.rpc,
opts.fromBlock,
latestBlock,
[TOPIC_UNBLACKLISTED_CORRECT]
),
]);
console.log(` Blacklisted events : ${blacklistedLogs.length}`);
console.log(` UnBlacklisted events : ${unblacklistedLogs.length}`);
// ── 2. Build event-derived state ──────────────────────────────────────────
// addr → { blacklistCount, unblacklistCount, lastBlacklistBlock, lastUnblacklistBlock }
const eventState = new Map();
for (const log of blacklistedLogs) {
const addr = decodeAddress(log.topics[1] || log.data);
if (!eventState.has(addr)) eventState.set(addr, { blacklistCount: 0, unblacklistCount: 0, lastBlacklistBlock: 0, lastUnblacklistBlock: 0 });
const s = eventState.get(addr);
s.blacklistCount++;
s.lastBlacklistBlock = Math.max(s.lastBlacklistBlock, parseInt(log.blockNumber, 16));
}
for (const log of unblacklistedLogs) {
const addr = decodeAddress(log.topics[1] || log.data);
if (!eventState.has(addr)) eventState.set(addr, { blacklistCount: 0, unblacklistCount: 0, lastBlacklistBlock: 0, lastUnblacklistBlock: 0 });
const s = eventState.get(addr);
s.unblacklistCount++;
s.lastUnblacklistBlock = Math.max(s.lastUnblacklistBlock, parseInt(log.blockNumber, 16));
}
// Derive expected state: net blacklisted if last blacklist event is more recent than last unblacklist
const addresses = [...eventState.keys()];
console.log(`\n Unique addresses seen in events: ${addresses.length}`);
const expectedBlacklisted = addresses.filter((a) => {
const s = eventState.get(a);
return s.lastBlacklistBlock > s.lastUnblacklistBlock;
});
const expectedUnblacklisted = addresses.filter((a) => {
const s = eventState.get(a);
return s.lastUnblacklistBlock >= s.lastBlacklistBlock && s.unblacklistCount > 0;
});
console.log(` Expected still blacklisted (by events) : ${expectedBlacklisted.length}`);
console.log(` Expected cleared (UnBlacklisted event) : ${expectedUnblacklisted.length}`);
// ── 3. Verify on-chain ────────────────────────────────────────────────────
console.log(`\n[2/3] Verifying all ${addresses.length} addresses on-chain (concurrency=${opts.concurrency})...`);
let done = 0;
const verifiedResults = await mapConcurrent(addresses, opts.concurrency, async (addr) => {
const onChain = await checkIsBlacklisted(opts.rpc, addr);
done++;
if (done % 25 === 0 || done === addresses.length)
process.stdout.write(`\r Progress: ${done}/${addresses.length} `);
const evState = eventState.get(addr);
const evExpects = evState.lastBlacklistBlock > evState.lastUnblacklistBlock;
const mismatch = onChain !== null && onChain !== evExpects;
return {
address: addr,
onChainBlacklisted: onChain,
eventExpected: evExpects,
mismatch,
blacklistCount: evState.blacklistCount,
unblacklistCount: evState.unblacklistCount,
lastBlacklistBlock: evState.lastBlacklistBlock,
lastUnblacklistBlock: evState.lastUnblacklistBlock,
};
});
process.stdout.write("\n");
// ── 4. OFAC cross-reference (optional) ───────────────────────────────────
let ofacSet = new Set();
if (opts.withOfac) {
console.log("\n[3/3] Fetching OFAC sanctioned addresses...");
ofacSet = await fetchOfacAddresses();
console.log(` OFAC addresses loaded: ${ofacSet.size}`);
}
// Attach OFAC flag
const results = verifiedResults.map((r) => ({
...r,
ofacSanctioned: ofacSet.size > 0 ? ofacSet.has(r.address) : null,
}));
// ── 5. Analysis ───────────────────────────────────────────────────────────
const stillBlacklisted = results.filter((r) => r.onChainBlacklisted === true);
const confirmedCleared = results.filter((r) => r.onChainBlacklisted === false && r.unblacklistCount > 0);
const mismatches = results.filter((r) => r.mismatch);
const multiBlacklisted = results.filter((r) => r.blacklistCount > 1);
const rpcErrors = results.filter((r) => r.onChainBlacklisted === null);
console.log("\n─".repeat(60));
console.log("📊 Results");
console.log("─".repeat(60));
console.log(` Total addresses in event history : ${results.length}`);
console.log(` ✅ Currently blacklisted on-chain : ${stillBlacklisted.length}`);
console.log(` 🔓 Confirmed reversed (UnBlacklisted) : ${confirmedCleared.length}`);
console.log(` ⚠️ Event/on-chain state mismatches : ${mismatches.length}`);
console.log(` 🔁 Blacklisted more than once : ${multiBlacklisted.length}`);
console.log(` ❓ RPC errors (could not verify) : ${rpcErrors.length}`);
if (ofacSet.size > 0) {
const ofacAndBlacklisted = results.filter((r) => r.ofacSanctioned && r.onChainBlacklisted);
const ofacButCleared = results.filter((r) => r.ofacSanctioned && r.onChainBlacklisted === false);
console.log(`\n OFAC sanctioned + still blacklisted : ${ofacAndBlacklisted.length}`);
console.log(` OFAC sanctioned but CLEARED on-chain : ${ofacButCleared.length}`);
}
// Print reversed addresses
if (confirmedCleared.length > 0) {
console.log(`\n🔓 Reversed addresses (had Blacklisted event, now clear):`);
confirmedCleared.slice(0, 20).forEach((r) => {
console.log(
` ${r.address} ` +
`[blacklisted ${r.blacklistCount}x, unblacklisted ${r.unblacklistCount}x]` +
(r.ofacSanctioned ? " ⚠️ OFAC" : "")
);
});
if (confirmedCleared.length > 20)
console.log(` ... and ${confirmedCleared.length - 20} more`);
}
if (mismatches.length > 0) {
console.log(`\n⚠️ State mismatches (event says X but chain says Y):`);
mismatches.forEach((r) => {
console.log(
` ${r.address} event=${r.eventExpected ? "blacklisted" : "cleared"} ` +
`chain=${r.onChainBlacklisted ? "blacklisted" : "cleared"}`
);
});
}
// ── Single address check ──────────────────────────────────────────────────
if (opts.check) {
console.log("\n─".repeat(60));
const hit = results.find((r) => r.address === opts.check);
if (hit) {
console.log(`\n🔎 ${opts.check}`);
console.log(` On-chain blacklisted : ${hit.onChainBlacklisted}`);
console.log(` Blacklist events : ${hit.blacklistCount}`);
console.log(` UnBlacklist events : ${hit.unblacklistCount}`);
console.log(` Last blacklist block : ${hit.lastBlacklistBlock}`);
console.log(` Last unblacklist blk : ${hit.lastUnblacklistBlock || "n/a"}`);
if (hit.ofacSanctioned !== null)
console.log(` OFAC sanctioned : ${hit.ofacSanctioned}`);
} else {
console.log(`\n Address not found in event history. Running direct on-chain check...`);
const live = await checkIsBlacklisted(opts.rpc, opts.check);
console.log(` On-chain isBlacklisted: ${live}`);
}
return;
}
// ── Print currently blacklisted ───────────────────────────────────────────
if (opts.printBlacklisted) {
console.log(`\n${"─".repeat(60)}`);
console.log(`✅ Currently blacklisted addresses (${stillBlacklisted.length} total):\n`);
stillBlacklisted.forEach((r) => {
const ofac = r.ofacSanctioned ? " [OFAC]" : "";
const flipped = r.unblacklistCount > 0 ? " [re-blacklisted]" : "";
console.log(` ${r.address}${ofac}${flipped}`);
});
console.log();
return;
}
// ── Output ────────────────────────────────────────────────────────────────
if (opts.output === "json") {
const outPath = path.resolve("sanctioned.json");
fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
console.log(`\n💾 Saved → ${outPath}`);
} else if (opts.output === "csv") {
const outPath = path.resolve("sanctioned.csv");
const header = "address,onChainBlacklisted,eventExpected,mismatch,blacklistCount,unblacklistCount,lastBlacklistBlock,lastUnblacklistBlock,ofacSanctioned";
const rows = results.map((r) =>
[
r.address,
r.onChainBlacklisted,
r.eventExpected,
r.mismatch,
r.blacklistCount,
r.unblacklistCount,
r.lastBlacklistBlock,
r.lastUnblacklistBlock || "",
r.ofacSanctioned ?? "",
].join(",")
);
fs.writeFileSync(outPath, [header, ...rows].join("\n"));
console.log(`\n💾 Saved → ${outPath}`);
} else {
console.log("\n💡 Commands:");
console.log(" node getSanctionedAddresses.js --rpc <URL> --print-blacklisted");
console.log(" node getSanctionedAddresses.js --rpc <URL> --output json");
console.log(" node getSanctionedAddresses.js --rpc <URL> --output csv");
console.log(" node getSanctionedAddresses.js --rpc <URL> --with-ofac --output json");
console.log(" node getSanctionedAddresses.js --rpc <URL> --check 0xde787f...\n");
}
}
main().catch((err) => {
console.error("\nFatal:", err.message);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment