feat(security): add GoldRush pre-transfer spam detection#27
feat(security): add GoldRush pre-transfer spam detection#27dinxsh wants to merge 2 commits intoelizaos-plugins:1.xfrom
Conversation
…hment Fixes elizaos-plugins#15 Extends WalletProvider to optionally fetch full token balances across all configured chains via GoldRush API when GOLDRUSH_API_KEY is set. Falls back to existing behavior if key is not present — zero breaking changes. Why GoldRush: - Single API covers 100+ EVM chains - Returns USD values, spam filtering, and decoded token metadata - Parallel fetching across chains in one provider call Configuration: GOLDRUSH_API_KEY=your_key_here # optional Without key: existing behavior unchanged With key: agent context includes full multi-chain token portfolio Tested on: eth-mainnet, base-mainnet, arbitrum-mainnet, matic-mainnet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds pre-transfer and pre-swap spam validation using GoldRush Enhanced Spam Lists, protecting agents from interacting with known rug tokens. - HIGH confidence spam (Confidence.YES): blocks transaction with a clear error - MEDIUM confidence spam (Confidence.MAYBE): warns via elizaLogger, allows transaction - Fully optional: requires GOLDRUSH_API_KEY, fails open if unavailable or unsupported chain - Zero breaking changes to existing transfer/swap behavior New file: src/utils/spamCheck.ts - checkTokenSpam(tokenAddress, chainName, apiKey) → SpamCheckResult - Maps viem chain names → goldrush-enhanced-spam-lists network identifiers - Skips native tokens (zero address / "native") automatically - Fail-open: any error in the spam check allows the transaction through Modified: src/actions/transfer.ts - Runs checkTokenSpam on params.token if it is an ERC20 address - Blocks HIGH risk; logs warn for MEDIUM risk Modified: src/actions/swap.ts - Runs checkTokenSpam on the resolved destination token address - Blocks HIGH risk; logs warn for MEDIUM risk New file: src/tests/spamCheck.test.ts - 13 vitest unit tests covering all behavior rules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughThis PR integrates GoldRush spam detection and multi-chain balance tracking into the EVM plugin. It adds optional token spam validation to swap and transfer actions, introduces multi-chain balance retrieval capabilities, and includes comprehensive test coverage for the new spam-checking utility. Changes
Sequence DiagramssequenceDiagram
participant User
participant Action as Swap/Transfer Action
participant SpamUtil as checkTokenSpam Utility
participant GoldRush as GoldRush API
participant Chain as Blockchain
User->>Action: Initiate swap/transfer
Action->>SpamUtil: checkTokenSpam(token, chain, apiKey)
alt API Key Missing or Native Token
SpamUtil-->>Action: UNKNOWN risk, no block
else Valid ERC20 Token
SpamUtil->>GoldRush: isERC20Spam(Confidence.YES)
GoldRush-->>SpamUtil: true
SpamUtil-->>Action: HIGH risk, shouldBlock=true
Action-->>User: ❌ Blocked - HIGH spam risk
else Not HIGH Risk
SpamUtil->>GoldRush: isERC20Spam(Confidence.MAYBE)
GoldRush-->>SpamUtil: true
SpamUtil-->>Action: MEDIUM risk, shouldBlock=false
Action->>Action: ⚠️ Log warning
Action->>Chain: Proceed with operation
Chain-->>User: ✓ Completed
else Clean Token
SpamUtil->>GoldRush: Confidence checks return false
SpamUtil-->>Action: LOW risk, proceed
Action->>Chain: Execute operation
Chain-->>User: ✓ Completed
end
sequenceDiagram
participant Client
participant WalletProvider as evmWalletProvider
participant EVM as EVM Service
participant GoldRush as GoldRush Client
Client->>WalletProvider: get(walletAddress)
alt Cached Data Exists
WalletProvider->>WalletProvider: Build initial balance text
alt API Key Configured
WalletProvider->>GoldRush: getMultiChainBalances(address, key)
GoldRush-->>WalletProvider: { chains: {balances, USD value} }
WalletProvider->>WalletProvider: buildGoldRushText(results)
WalletProvider->>WalletProvider: Augment text with multi-chain data
else No API Key
WalletProvider->>WalletProvider: Use base balance text
end
WalletProvider-->>Client: ProviderResult(text, data, values)
else No Cached Data
WalletProvider->>EVM: Direct balance fetch
EVM-->>WalletProvider: Balance data
WalletProvider-->>Client: ProviderResult
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| const getMultiChainBalances = async (address: string, chains: string[], apiKey: string) => { | ||
| const client = new GoldRushClient(apiKey); |
There was a problem hiding this comment.
New GoldRushClient created on every call
getMultiChainBalances is called from buildGoldRushText, which runs inside evmWalletProvider.get() — a provider that executes on every agent message cycle. Each invocation constructs a new GoldRushClient(apiKey), which likely sets up internal HTTP connection state. Consider hoisting the client into a module-level singleton (or a lazy-initialized cache keyed by apiKey) to avoid repeated instantiation on every provider call.
| const getMultiChainBalances = async (address: string, chains: string[], apiKey: string) => { | |
| const client = new GoldRushClient(apiKey); | |
| const getMultiChainBalances = async (address: string, chains: string[], client: GoldRushClient) => { |
| "dependencies": { | ||
| "@covalenthq/client-sdk": "^3.0.5", |
There was a problem hiding this comment.
Missing GOLDRUSH_API_KEY in agentConfig
The agentConfig.pluginParameters section documents every other environment variable, but GOLDRUSH_API_KEY is absent. This means ElizaOS agent auto-configuration won't surface this optional setting to users. Add an entry like:
"GOLDRUSH_API_KEY": {
"type": "string",
"description": "Optional GoldRush API key to enable multi-chain token balance enrichment and pre-transfer spam detection.",
"required": false,
"sensitive": true
}| export async function checkTokenSpam( | ||
| tokenAddress: string, | ||
| chainName: string, | ||
| apiKey: string | ||
| ): Promise<SpamCheckResult> { |
There was a problem hiding this comment.
apiKey accepted but never passed to isERC20Spam
The apiKey parameter is used only as a feature gate (line 40), but is never forwarded to isERC20Spam. The @covalenthq/goldrush-enhanced-spam-lists package is a free public good that doesn't require authentication — so this works. However, gating the spam check on GOLDRUSH_API_KEY means spam detection is silently disabled when users only want balance enrichment disabled but might still benefit from free spam protection. Consider either:
- Documenting that the spam check is free and doesn't need the key, or
- Using a separate feature flag (e.g.,
EVM_SPAM_CHECK_ENABLED) so spam protection can work independently of the paid GoldRush balance API.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/providers/wallet.ts (1)
63-91: Consider adding type safety for GoldRush response items.The
item: anytype in the reduce callback (line 81) loses type safety. If the GoldRush SDK provides types for balance items, consider using them to catch potential issues at compile time.♻️ Suggested improvement
- totalUSD: (resp.data?.items ?? []).reduce( - (sum: number, item: any) => sum + (item.quote ?? 0), - 0 - ), + totalUSD: (resp.data?.items ?? []).reduce( + (sum, item) => sum + (item.quote ?? 0), + 0 + ),If the SDK exports types like
BalanceItem, use those for the response data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/providers/wallet.ts` around lines 63 - 91, The reduce callback in getMultiChainBalances uses item: any which loses type safety; import and use the SDK's BalanceItem (or equivalent) type for the response and annotate the response and intermediate values (e.g., the return shape of BalanceService.getTokenBalancesForWalletAddress, resp.data.items, and the Promise result) so the reducer becomes (item: BalanceItem) and TypeScript can check item.quote, item.symbol, etc.; update types on the mapped return object (chain, items, totalUSD) and any Promise generics to propagate the correct types through getMultiChainBalances, CHAIN_MAP usage, and the Promise.allSettled mapping.src/utils/spamCheck.ts (1)
60-81: Two sequential API calls for non-HIGH-risk tokens.The current implementation makes two separate
isERC20Spamcalls for tokens that aren't HIGH risk. Consider whether the library offers a way to get the confidence level in a single call, or document that this is intentional for accuracy.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/spamCheck.ts` around lines 60 - 81, The code currently calls isERC20Spam twice (once with Confidence.YES and once with Confidence.MAYBE); replace this with a single call that returns the confidence level (or use the library's method that returns a Confidence enum/value) and switch on that result to return the appropriate object, referencing isERC20Spam (or the library method that provides confidence) and the Confidence enum, tokenAddress and spamNetwork; if the library doesn't provide a single-call confidence result, add a comment documenting why two calls are required and why we chose accuracy over an extra API hit.package.json (1)
25-26: Consider pinning@covalenthq/goldrush-enhanced-spam-liststo an exact version.The
@covalenthq/goldrush-enhanced-spam-listspackage is at version0.0.1and is the only version released. While using^0.0.1allows patch updates under semver, pinning to exactly0.0.1is a best practice for pre-1.0 packages to avoid unexpected breakages if future minor versions are released with breaking changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 25 - 26, The package entry for "@covalenthq/goldrush-enhanced-spam-lists" uses a caret range ("^0.0.1"); change it to an exact pinned version ("0.0.1") in package.json to avoid automatic updates for this pre-1.0 package—update the dependency value for "@covalenthq/goldrush-enhanced-spam-lists" from "^0.0.1" to "0.0.1" and run your package manager to lock the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/providers/wallet.ts`:
- Around line 106-120: The loop over chainResults may call BigInt(item.balance)
with malformed values; update the inner loop (referencing chainResults,
item.balance, formatUnits, lines, totalPortfolioUSD) to defensively validate
that item.balance is a valid integer string before converting: skip or coerce
non-string/null/undefined values and guard against invalid formats (e.g., use a
regex like /^[+-]?\d+$/ or attempt parsing inside a try/catch), only call BigInt
and formatUnits when validation succeeds, and otherwise continue the loop
(optionally log or treat quote as zero) so no runtime exception is thrown.
- Around line 38-61: Update the incorrect GoldRush chain identifiers in the
SPAM_CHAIN_MAP constant in spamCheck.ts so they match the canonical values used
in wallet.ts (replace 'op-mainnet' with 'optimism-mainnet' and 'pol-mainnet'
with 'matic-mainnet'); then update the expectations in spamCheck.test.ts (the
assertions around lines validating SPAM_CHAIN_MAP) to expect 'optimism-mainnet'
and 'matic-mainnet' instead of the old values so the spam list checks and tests
align with GoldRush API identifiers.
---
Nitpick comments:
In `@package.json`:
- Around line 25-26: The package entry for
"@covalenthq/goldrush-enhanced-spam-lists" uses a caret range ("^0.0.1"); change
it to an exact pinned version ("0.0.1") in package.json to avoid automatic
updates for this pre-1.0 package—update the dependency value for
"@covalenthq/goldrush-enhanced-spam-lists" from "^0.0.1" to "0.0.1" and run your
package manager to lock the change.
In `@src/providers/wallet.ts`:
- Around line 63-91: The reduce callback in getMultiChainBalances uses item: any
which loses type safety; import and use the SDK's BalanceItem (or equivalent)
type for the response and annotate the response and intermediate values (e.g.,
the return shape of BalanceService.getTokenBalancesForWalletAddress,
resp.data.items, and the Promise result) so the reducer becomes (item:
BalanceItem) and TypeScript can check item.quote, item.symbol, etc.; update
types on the mapped return object (chain, items, totalUSD) and any Promise
generics to propagate the correct types through getMultiChainBalances, CHAIN_MAP
usage, and the Promise.allSettled mapping.
In `@src/utils/spamCheck.ts`:
- Around line 60-81: The code currently calls isERC20Spam twice (once with
Confidence.YES and once with Confidence.MAYBE); replace this with a single call
that returns the confidence level (or use the library's method that returns a
Confidence enum/value) and switch on that result to return the appropriate
object, referencing isERC20Spam (or the library method that provides confidence)
and the Confidence enum, tokenAddress and spamNetwork; if the library doesn't
provide a single-call confidence result, add a comment documenting why two calls
are required and why we chose accuracy over an extra API hit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 44586ca6-ca3d-4487-8d7b-e40f503502d2
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
README.mdpackage.jsonsrc/actions/swap.tssrc/actions/transfer.tssrc/providers/wallet.tssrc/tests/spamCheck.test.tssrc/utils/spamCheck.ts
| // Mapping from viem chain names to GoldRush chain names | ||
| const CHAIN_MAP: Record<string, string> = { | ||
| mainnet: 'eth-mainnet', | ||
| base: 'base-mainnet', | ||
| arbitrum: 'arbitrum-mainnet', | ||
| optimism: 'optimism-mainnet', | ||
| polygon: 'matic-mainnet', | ||
| bsc: 'bsc-mainnet', | ||
| avalanche: 'avalanche-mainnet', | ||
| fantom: 'fantom-mainnet', | ||
| gnosis: 'gnosis-mainnet', | ||
| celo: 'celo-mainnet', | ||
| moonbeam: 'moonbeam-mainnet', | ||
| moonriver: 'moonriver-mainnet', | ||
| cronos: 'cronos-mainnet', | ||
| klaytn: 'klaytn-mainnet', | ||
| aurora: 'aurora-mainnet', | ||
| linea: 'linea-mainnet', | ||
| scroll: 'scroll-mainnet', | ||
| mantle: 'mantle-mainnet', | ||
| zksync: 'zksync-mainnet', | ||
| polygonZkEvm: 'polygon-zkevm-mainnet', | ||
| arbitrumNova: 'arbitrum-nova-mainnet', | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
GoldRush API chain identifiers eth-mainnet op-mainnet optimism-mainnet
💡 Result:
GoldRush API chainName identifiers:
| Network | GoldRush chainName | Numeric chain ID |
|---|---|---|
| Ethereum Mainnet | eth-mainnet |
1 [1] |
| OP Mainnet (Optimism) | optimism-mainnet |
10 [2] |
op-mainnet is not listed as a GoldRush chainName; for OP Mainnet you should use optimism-mainnet. [3]
Sources: [1] [2] [3]
🏁 Script executed:
# Find and read the spamCheck.ts file
fd -t f "spamCheck" --exec cat -n {} \;Repository: elizaos-plugins/plugin-evm
Length of output: 10654
🌐 Web query:
GoldRush API Polygon chain identifier matic-mainnet pol-mainnet
💡 Result:
In the GoldRush (Covalent) API, Polygon PoS mainnet’s chain identifier is matic-mainnet (chain ID 137). GoldRush lets you use either the chain name or the numeric chain ID in requests. pol-mainnet is not the Polygon chain name in GoldRush. [1][2]
Example (using chain name): .../v1/matic-mainnet/...
Example (using chain id): .../v1/137/... [1][2]
Sources: [1] [2]
Critical bug: SPAM_CHAIN_MAP in spamCheck.ts uses incorrect GoldRush chain identifiers.
The identifiers in SPAM_CHAIN_MAP (lines 17-18) are invalid for GoldRush API:
op-mainnet→ should beoptimism-mainnetpol-mainnet→ should bematic-mainnet
The correct identifiers match those in wallet.ts. This bug will cause spam list checks to fail silently. The tests (spamCheck.test.ts, lines 94-108) also validate these incorrect identifiers.
Update SPAM_CHAIN_MAP and corresponding test expectations to use optimism-mainnet and matic-mainnet.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/providers/wallet.ts` around lines 38 - 61, Update the incorrect GoldRush
chain identifiers in the SPAM_CHAIN_MAP constant in spamCheck.ts so they match
the canonical values used in wallet.ts (replace 'op-mainnet' with
'optimism-mainnet' and 'pol-mainnet' with 'matic-mainnet'); then update the
expectations in spamCheck.test.ts (the assertions around lines validating
SPAM_CHAIN_MAP) to expect 'optimism-mainnet' and 'matic-mainnet' instead of the
old values so the spam list checks and tests align with GoldRush API
identifiers.
| for (const result of chainResults) { | ||
| for (const item of result.items) { | ||
| if (!item.balance || item.balance === '0') continue; | ||
| const symbol = item.contract_ticker_symbol ?? '???'; | ||
| const decimals = item.contract_decimals ?? 18; | ||
| const balance = formatUnits(BigInt(item.balance), decimals); | ||
| const formattedBalance = Number(balance).toLocaleString('en-US', { | ||
| maximumFractionDigits: 4, | ||
| }); | ||
| const usdValue = item.quote ?? 0; | ||
| const formattedUSD = `$${usdValue.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; | ||
| lines.push(`${symbol} (${result.chain}): ${formattedBalance} ${symbol} — ${formattedUSD}`); | ||
| totalPortfolioUSD += usdValue; | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential runtime error if item.balance is not a valid BigInt string.
Line 111 calls BigInt(item.balance) which will throw if balance is not a valid numeric string (e.g., if it's null, undefined, or malformed). The check on line 108 guards against '0' and falsy values but doesn't validate the string format.
🛡️ Suggested defensive fix
for (const item of result.items) {
if (!item.balance || item.balance === '0') continue;
+ let balance: string;
+ try {
+ balance = formatUnits(BigInt(item.balance), decimals);
+ } catch {
+ continue; // Skip items with invalid balance format
+ }
const symbol = item.contract_ticker_symbol ?? '???';
const decimals = item.contract_decimals ?? 18;
- const balance = formatUnits(BigInt(item.balance), decimals);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/providers/wallet.ts` around lines 106 - 120, The loop over chainResults
may call BigInt(item.balance) with malformed values; update the inner loop
(referencing chainResults, item.balance, formatUnits, lines, totalPortfolioUSD)
to defensively validate that item.balance is a valid integer string before
converting: skip or coerce non-string/null/undefined values and guard against
invalid formats (e.g., use a regex like /^[+-]?\d+$/ or attempt parsing inside a
try/catch), only call BigInt and formatUnits when validation succeeds, and
otherwise continue the loop (optionally log or treat quote as zero) so no
runtime exception is thrown.
Closes #15
Summary
src/utils/spamCheck.ts— a reusablecheckTokenSpam()utility backed by@covalenthq/goldrush-enhanced-spam-listsTransferAction.transfer()to block/warn on spam ERC20 tokensSwapAction.swap()to block/warn on spam destination tokensBehavior
Confidence.YES(HIGH)Confidence.MAYBE(MEDIUM)elizaLogger.warn— transaction proceeds"native")Configuration
Supported chains for spam detection
eth-mainnet,base-mainnet,op-mainnet,pol-mainnet,bsc-mainnet,gnosis-mainnetTokens on other chains pass through unchanged.
Files changed
src/utils/spamCheck.tscheckTokenSpam()utilitysrc/tests/spamCheck.test.tssrc/actions/transfer.tsparams.tokensrc/actions/swap.tstoTokenpackage.json/bun.lock@covalenthq/goldrush-enhanced-spam-listsTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Greptile Summary
This PR adds two GoldRush-powered features to the EVM plugin: (1) a pre-transfer/pre-swap spam detection gate using
@covalenthq/goldrush-enhanced-spam-liststhat blocks HIGH-confidence spam tokens and warns on MEDIUM, and (2) multi-chain token balance enrichment in the wallet provider using@covalenthq/client-sdk. Both features are opt-in via theGOLDRUSH_API_KEYenv var and fail open gracefully.spamCheck.ts): Clean utility with well-defined risk levels (HIGH blocks, MEDIUM warns, UNKNOWN passes through). Integrated into bothTransferAction.transfer()andSwapAction.swap(). Covers 6 chains.wallet.ts): AddsGoldRushClientusage to append token holdings with USD values to the wallet provider output. However, a newGoldRushClientis instantiated on every provider call — should be cached.agentConfigentry:GOLDRUSH_API_KEYis not listed inpackage.json'sagentConfig.pluginParameters, inconsistent with all other env vars in this plugin.Confidence Score: 3/5
src/providers/wallet.ts(GoldRushClient instantiated per-call),package.json(missing agentConfig entry for GOLDRUSH_API_KEY)Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Transfer/Swap Action called] --> B{GOLDRUSH_API_KEY set?} B -- No --> G[Proceed with transaction] B -- Yes --> C{Token is native/zero address?} C -- Yes --> G C -- No --> D[checkTokenSpam] D --> E{Spam confidence?} E -- HIGH --> F[Throw Error - Transaction Blocked] E -- MEDIUM --> H[elizaLogger.warn] --> G E -- LOW/UNKNOWN --> G E -- API Error --> G I[Wallet Provider get] --> J{GOLDRUSH_API_KEY set?} J -- No --> K[Return native balances only] J -- Yes --> L[GoldRushClient.BalanceService] --> M[Append token holdings to result] M --> KLast reviewed commit: 19cbf70
(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!