import type { Flag, IssuerPowers, IssuerActivity, Verdict, Variant } from "./types.js"; import { exactSymbolMatch, nearestSymbol, hasNonAscii, type CanonicalAsset } from "./knownAssets.js"; export const UINT128_MAX = (1n << 128n) - 1n; export const UINT256_MAX = (1n << 256n) - 1n; /** A supply cap at/above the uint128 sentinel means "effectively uncapped". */ export function isUncapped(cap: bigint): boolean { return cap >= UINT128_MAX; } export interface AnalyzerInput { isNativeB20: boolean; isCopycat: boolean; // B20-shaped address that was NOT created by the factory (real bytecode) hasBytecodeButPrefix: boolean; variant: Variant | null; name: string | null; symbol: string | null; powers: IssuerPowers | null; activity: IssuerActivity | null; creatorNonce: number | null; // tx count of the creator address at head viaLauncher: boolean; launcher: string | null; // Distinguishes actively-held/configured powers from latent admin power (for precise flags). seizeDedicated: boolean; pauseDedicated: boolean; freezeConfigured: boolean; /** Set by callers via canonicalAssetAt(address, network): the address IS a known asset's * canonical Base mainnet deployment. Optional so an unaware caller fails toward over-flagging * (imposter checks run), never toward clearing a scam. */ canonicalAsset?: CanonicalAsset | null; /** False when the address has NO deployed contract code (EOA / undeployed / bare 0xef stub): * an unreadable symbol there is expected, not suspicious, so SYMBOL_UNREADABLE is suppressed. * Optional, defaulting to true, so an unaware caller fails toward over-flagging. */ hasCode?: boolean; } const HIGH_POWER_CODES = new Set(["CAN_SEIZE", "FREEZE_ACTIVE"]); /** Derive risk flags. Pure. */ export function deriveFlags(input: AnalyzerInput): Flag[] { const flags: Flag[] = []; if (input.isCopycat) { flags.push({ code: "COPYCAT_NON_NATIVE", detail: "Address matches the B20 address prefix but was NOT created by the B20 factory (has deployed EVM bytecode instead of the native 0xef stub). This is a contract impersonating a native B20 token.", severity: "high", }); } // Canonical allowlist: strictly address-matched, and honored ONLY for a plain non-B20 // (a native B20 or a copycat at a canonical address is impossible today, but if it ever // happened the imposter/copycat logic must win, so the guard stays). const canonical = !input.isNativeB20 && !input.isCopycat ? (input.canonicalAsset ?? null) : null; if (canonical) { flags.push({ code: "CANONICAL_ASSET", detail: `Address is the canonical Base deployment of '${canonical.name}' (${canonical.symbol}), verified by contract address against ${canonical.source}. Not a B20 token.`, severity: "info", }); } // A symbol that could not be read (reverting/absent symbol(), decode failure) cannot pass the // imposter check, so the token must NOT sign a clean verdict: flag it, and the medium severity // rolls the verdict up to at least caution. Total RPC failures throw upstream (fail-loud); // a null here means the CONTRACT itself would not yield a symbol at the pinned block. // Codeless addresses (EOA/undeployed) are exempt: not a token, nothing to impersonate with. if (input.symbol === null && (input.hasCode ?? true)) { flags.push({ code: "SYMBOL_UNREADABLE", detail: "The token's symbol() could not be read (reverts, is absent, or returns a non-string). The known-asset imposter check cannot run, so this result must not be treated as a clean pass.", severity: "medium", }); } // Symbol-collision checks run for ANY token (native, copycat, or plain non-B20): a contract // carrying a well-known asset symbol is impersonation regardless of what it technically is. // Exception: the canonical deployment itself is the genuine article, not an imposter. const symbol = canonical ? "" : (input.symbol ?? ""); const exact = exactSymbolMatch(symbol); const near = nearestSymbol(symbol, 1); const nonAscii = hasNonAscii(symbol); if (exact) { flags.push({ code: "NAME_IMPOSTER", detail: `Symbol '${symbol}' collides with well-known asset '${exact.name}' (${exact.symbol}). Unless the creator is the genuine issuer, this token is impersonating a known asset.`, severity: "high", }); } else if (near && nonAscii) { // A NON-ASCII symbol within 1 edit of a known asset is impersonation (homoglyph), not a typo: // legitimate tickers are ASCII. Treat as an imposter regardless of confusables-table coverage. flags.push({ code: "NAME_IMPOSTER", detail: `Symbol '${symbol}' uses non-ASCII characters and is a look-alike of well-known asset '${near.asset.symbol}' (${near.asset.name}); homoglyph impersonation.`, severity: "high", }); } else if (near) { flags.push({ code: "SYMBOL_LOOKALIKE", detail: `Symbol '${symbol}' is 1 edit away from well-known asset '${near.asset.symbol}' (${near.asset.name}); possible typosquat.`, severity: "medium", }); } if (!input.isNativeB20) { if (!input.isCopycat) { flags.push({ code: "NOT_B20", detail: "Address is not a native B20 token (no factory record).", severity: "info", }); } return flags; } // From here on: native B20 power flags. const p = input.powers; if (p) { // A blocklist is enforced RIGHT NOW — addresses can be (or are) frozen. Higher signal than latent. if (input.freezeConfigured) flags.push({ code: "FREEZE_ACTIVE", detail: "A transfer blocklist policy is currently enforced; holders can be frozen out of transfers.", severity: "high", }); // A dedicated seizure-role holder exists (beyond the admin's latent ability to grant it). if (input.seizeDedicated) flags.push({ code: "CAN_SEIZE", detail: "A BURN_BLOCKED_ROLE holder can burn tokens from blocklisted addresses (seizure).", severity: "medium", }); if (input.pauseDedicated) flags.push({ code: "CAN_PAUSE", detail: "A PAUSE_ROLE holder can halt transfers/mints/burns.", severity: "low", }); if (p.canMint && p.supplyCapKnown && isUncappedStr(p.supplyCap)) flags.push({ code: "UNCAPPED_MINT", detail: "Supply is uncapped and a live minter exists (a MINT_ROLE holder, or an active DEFAULT_ADMIN who can grant it); total supply can be inflated at will.", severity: "medium", }); if (p.transferRestrictions) flags.push({ code: "TRANSFER_RESTRICTED", detail: "Transfers are gated by an allow/blocklist policy; holders may be unable to move tokens freely.", severity: "low", }); // A live admin has near-total control: it can grant itself mint/burn/pause/seize roles and // call updatePolicy (install a freeze blocklist) / updateSupplyCap at will. This is why the // canX powers above are true even without a dedicated holder. if (p.adminActive) flags.push({ code: "ADMIN_ACTIVE", detail: "A DEFAULT_ADMIN holder exists: it can mint, pause, seize, install a transfer blocklist (freeze), and re-cap supply at will.", severity: "medium", }); } if (input.activity && input.activity.mints === 0 && input.activity.lastActionBlock === null) { flags.push({ code: "ZERO_ACTIVITY", detail: "No issuer activity observed since creation (no mints, pauses, seizures, or role changes).", severity: "info", }); } if (input.creatorNonce !== null && input.creatorNonce <= 3) { flags.push({ code: "FRESH_CREATOR", detail: `Creator address has a very low transaction count (nonce ${input.creatorNonce}); newly-created wallet.`, severity: "low", }); } if (input.viaLauncher) { flags.push({ code: "VIA_LAUNCHER", detail: `Created through an intermediary launcher contract${input.launcher ? ` (${input.launcher})` : ""}, not a direct factory call; on-chain creator is the launcher's msg.sender, not the tx initiator.`, severity: "info", }); } return flags; } function isUncappedStr(cap: string | null): boolean { return cap === null; } /** Roll flags up into a single verdict. Pure. */ export function deriveVerdict(input: AnalyzerInput, flags: Flag[]): Verdict { if (input.isCopycat) return "danger"; // A known-asset symbol collision is impersonation whether or not the address is a native B20: // a plain ERC-20 honeypot named "USDC" must never be signed affirmatively "ok". if (flags.some((f) => f.code === "NAME_IMPOSTER")) return "danger"; if (!input.isNativeB20) { // An unreadable symbol means the imposter check never ran: never a clean pass, even non-B20. if (flags.some((f) => f.code === "SYMBOL_UNREADABLE")) return "caution"; // A recognized canonical asset (e.g. native USDC) is a genuine safe positive. if (flags.some((f) => f.code === "CANONICAL_ASSET")) return "ok"; // Otherwise the address is simply outside B20 Check's scope. Returning "ok" here would let an // agent that reads only `verdict` treat "not a B20" as "safe"; not_applicable says exactly // what is true, that there is nothing B20-specific to vouch for. isB20:false remains the // machine-readable signal. return "not_applicable"; } const hasHighPower = flags.some((f) => HIGH_POWER_CODES.has(f.code)); const hasMediumSignal = flags.some( (f) => f.severity === "medium" || f.severity === "high", ); // An active admin can freeze/seize/mint an agent's position at will (latent power), so it is at // least caution — 'ok' is reserved for tokens where the issuer has NO such standing control. const adminExposure = input.powers?.adminActive === true || input.powers?.canFreeze === true || input.powers?.canSeize === true; if (hasMediumSignal || hasHighPower || adminExposure) return "caution"; return "ok"; }