Back to skill

Security audit

Goodwallet Trading

Security checks for vulnerabilities and agentic risk

Overview

This is a real GoodWallet trading skill, but it needs Review because it can sign broad on-chain actions with weak safeguards that could expose credentials or cause irreversible wallet mistakes.

Review carefully before installing. Use only testnet funds unless you have independently audited the package, avoid contract-call unless you can decode the calldata yourself, always specify exact approval amounts, do not rely on the documented slippage option for price protection, and do not set SIGN_URL or RPC_URL from untrusted shell profiles, wrappers, or CI environments.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
index.js:110
Finding
Wallet API Credential Exfiltration Through an Unvalidated Signing Endpoint<![CDATA[ ## Vulnerability Details **File Location**: `index.js:110-111`, `index.js:133-150` **Vulnerability Type**: Unvalidated credential destination / sensitive information disclosure **Risk Level**: High ### Vulnerable Code ```js const SIGN_URL = process.env.SIGN_URL || 'sign.goodwallet.dev'; const RELAY_URL = 'relay.' + SIGN_URL; ``` ```js async function signTransaction(config, unsignedSerializedTx) { const hash = keccak256(unsignedSerializedTx); const hashBytes = Buffer.from(hash.slice(2), 'hex'); const resp = await fetch(`https://${SIGN_URL}/agent/sign/ecdsa`, { method: 'POST', headers: { 'X-API-KEY': config.apiKey }, body: JSON.stringify({ hash: Buffer.from(hashBytes).toString('hex') }), }); if (!resp.ok) { const err = await resp.text(); throw new Error(`Sign API error (${resp.status}): ${err}`); } const { roomUuid } = await resp.json(); const ecdsa = new Ecdsa(RELAY_URL); const signature = await ecdsa.sign( roomUuid, config.share, new MessageHash(hashBytes), DERIVATION_PATH ); ``` ### Technical Analysis The signing service hostname is taken directly from the `SIGN_URL` environment variable without an allowlist or hostname validation. The wallet API key loaded from `~/.config/goodwallet/config.json` is subsequently placed in the `X-API-KEY` header and sent to that selected host. Although a configurable signing service is documented, transmitting an existing wallet credential to any environment-selected host exceeds least privilege. Environment variables can be influenced by shell profiles, CI configuration, wrapper scripts, compromised parent processes, or misleading invocation instructions. The same value is used to derive the MPC relay hostname. The wallet share is passed to the native signing SDK together with that relay address. The native SDK is outside this audit artifact, so its treatment of the share and network protocol cannot be verified here. ### Attack Path 1. An attacker causes the com ...[truncated 1399 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove arbitrary `SIGN_URL` overrides from normal operation and pin the production signing endpoint. 2. If custom signing infrastructure is required, enforce an exact allowlist of approved hostnames. 3. Parse endpoints with the standard `URL` class and reject: - Non-HTTPS schemes - Embedded credentials - Unexpected ports - IP literals - Unapproved subdomains - Path, query, or fragment components - Hostname suffix tricks 4. Do not reuse production API credentials with custom endpoints. Require separate credentials explicitly issued for each approved service. 5. Derive the API and relay URLs from separate trusted configuration entries rather than string concatenation. 6. Require explicit user confirmation before switching away from the default signing infrastructure. 7. Apply server-side credential scoping, expiration, revocation, transaction limits, and destination restrictions. 8. Document the native SDK's network behavior and verify that the wallet share never leaves the local process in recoverable form. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
index.js:400
Finding
DEX Swaps Disable Minimum-Output and Slippage Protection<![CDATA[ ## Vulnerability Details **File Location**: `index.js:400-448` **Vulnerability Type**: Unsafe transaction construction / missing price protection **Risk Level**: High ### Vulnerable Code ```js const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); // 20 min const slippageBps = Math.floor(parseFloat(options.slippage) * 100); const isFromETH = options.fromToken.toUpperCase() === 'ETH'; const isToETH = options.toToken.toUpperCase() === 'ETH'; if (isFromETH) { const amountIn = parseEther(options.amount); const path = [options.toToken]; // WETH is implicit in swapExactETHForTokens // For proper routing, the path should include WETH address console.log(`Swapping ${options.amount} ETH for tokens...`); const data = encodeFunctionData({ abi: UNISWAP_V2_ROUTER_ABI, functionName: 'swapExactETHForTokens', args: [0n, path, config.address, deadline], }); const txHash = await buildSignBroadcast(client, hoodi, config, { to: options.router, data, value: amountIn }); console.log(`Swap sent: ${txHash}`); } else if (isToETH) { const decimals = await client.readContract({ address: options.fromToken, abi: ERC20_ABI, functionName: 'decimals' }); const amountIn = parseUnits(options.amount, decimals); const path = [options.fromToken]; // path to ETH console.log(`Swapping ${options.amount} tokens for ETH...`); const data = encodeFunctionData({ abi: UNISWAP_V2_ROUTER_ABI, functionName: 'swapExactTokensForETH', args: [amountIn, 0n, path, config.address, deadline], }); const txHash = await buildSignBroadcast(client, hoodi, config, { to: options.router, data }); console.log(`Swap sent: ${txHash}`); } else { const decimals = await client.readContract({ address: options.fromToken, abi: ERC20_ABI, functionName: 'decimals' }); const amountIn = parseUnits(options.amount, decimals); const path = [options.fromToken, options.toToken]; console.log(`Swapping ${options.amount} tokens...`); ...[truncated 2022 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Call `getAmountsOut` using the exact intended route before constructing the transaction. 2. Calculate the minimum output using checked integer arithmetic: ```js const expectedOut = amounts[amounts.length - 1]; const amountOutMin = expectedOut * BigInt(10_000 - slippageBps) / 10_000n; ``` 3. Reject nonnumeric, negative, non-finite, or excessive slippage values. Apply a conservative maximum unless the user explicitly overrides it. 4. Include the wrapped-native token address in ETH routes: - ETH to token: `[WETH, outputToken]` - Token to ETH: `[inputToken, WETH]` 5. Validate the selected router against an allowlist for the configured chain. 6. Simulate the complete transaction before requesting an MPC signature. 7. Display the expected output, guaranteed minimum output, route, price impact, deadline, router, and network. 8. Require explicit confirmation for high price impact or unusual slippage. 9. Fail closed if quote retrieval, route validation, or simulation fails. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
index.js:328
Finding
Unlimited ERC20 Allowance Is Granted by Default<![CDATA[ ## Vulnerability Details **File Location**: `index.js:328-360` **Vulnerability Type**: Excessive token allowance / violation of least privilege **Risk Level**: High ### Vulnerable Code ```js program.command('approve') .description('Approve a spender for ERC20 tokens') .requiredOption('--token <address>', 'ERC20 token contract address') .requiredOption('--spender <address>', 'Spender address (e.g. DEX router)') .option('-a, --amount <amount>', 'Amount to approve (default: unlimited)') .option('--rpc <url>', 'RPC URL', HOODI_RPC) .action(async (options) => { const config = await loadConfig(); requireConfig(config); const client = createPublicClient({ chain: hoodi, transport: http(options.rpc) }); let amount; if (options.amount) { const decimals = await client.readContract({ address: options.token, abi: ERC20_ABI, functionName: 'decimals' }); amount = parseUnits(options.amount, decimals); } else { amount = 2n ** 256n - 1n; // max uint256 } const symbol = await client.readContract({ address: options.token, abi: ERC20_ABI, functionName: 'symbol' }).catch(() => 'TOKEN'); const data = encodeFunctionData({ abi: ERC20_ABI, functionName: 'approve', args: [options.spender, amount] }); console.log(`Approving ${options.amount || 'unlimited'} ${symbol} for ${options.spender}...`); ``` ### Technical Analysis If `--amount` is omitted, the command grants the spender the maximum possible ERC20 allowance. This makes unlimited authorization the default rather than an exceptional, explicit choice. ERC20 allowance permits the spender contract or account to call `transferFrom()` without another wallet signature. An unlimited allowance therefore remains dangerous after the immediate operation completes and may apply to tokens acquired in the future. The behavior is documented, but documentation does not make it least-privileged. Agent-generated commands are espe ...[truncated 1202 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make `--amount` mandatory for ordinary approvals. 2. Introduce a separate `--unlimited` flag for the exceptional maximum-allowance case. 3. Require explicit confirmation before an unlimited approval, showing: - Network and chain ID - Token contract - Spender address - Current allowance - New allowance - Risk to current and future balances 4. Validate known DEX routers against a chain-specific allowlist and warn prominently for unknown spenders. 5. Default to the exact amount required for the immediate operation. 6. Offer an approval-revocation command and recommend revocation after use. 7. For tokens requiring allowance reset, safely set the allowance to zero before setting a new nonzero value. 8. Simulate approval transactions and verify the token and spender are contracts where appropriate. ]]>

T08 · Insecure Dependencies

Warning
Location
index.js:24
Finding
Wallet Operations Depend on Runtime-Loaded Third-Party Native Code<![CDATA[ ## Vulnerability Details **File Location**: `index.js:24-45`, `package.json:12-14`, `SKILL.md:18-29` **Vulnerability Type**: Third-party and native dependency supply-chain exposure **Risk Level**: Medium ### Vulnerable Code ```js function getSodotNativePath() { const require = createRequire(import.meta.url); const gwPath = require.resolve('goodwallet'); const distDir = join(gwPath, '..'); // goodwallet/dist/ const targetMap = { 'darwin-x64': 'macos_x86_64', 'darwin-arm64': 'macos_arm64', 'linux-x64': 'linux_x86_64', 'linux-arm64': 'linux_arm64', }; const key = `${platform()}-${arch()}`; const target = targetMap[key]; if (!target) throw new Error(`Unsupported platform: ${key}`); return join(distDir, 'native', `libsodot_executor_${target}_nodejs.node`); } let _nativeSdk = null; function getNativeSdk() { if (!_nativeSdk) { const require = createRequire(import.meta.url); _nativeSdk = require(getSodotNativePath()); } return _nativeSdk; } ``` ```json "dependencies": { "goodwallet": "^0.2.0", "viem": "^2.28.0", "commander": "^13.1.0" } ``` The Skill documentation instructs runtime package execution: ```bash npx goodwallet-trading@0.2.0 npx goodwallet@0.2.0 auth npx goodwallet@0.2.0 pair ``` ### Technical Analysis The implementation dynamically locates and executes a platform-specific native Node.js module shipped by the external `goodwallet` package. Native modules execute with the full privileges of the Node.js process and are not constrained by JavaScript-level visibility. The dependency manifest uses caret ranges, permitting later compatible releases during installations that do not strictly honor the supplied lockfile. The documented `npx` workflow can also retrieve and execute npm packages at invocation time. The reviewed lockfile currently resolves packages from the official npm registry and pins `goodwallet` to version `0.2.0`; no typosquatted package or nonstandard registry was identifi ...[truncated 1522 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Use exact dependency versions rather than caret ranges. 2. Require deterministic installation with `npm ci` and the reviewed lockfile. 3. Avoid unattended runtime `npx` retrieval for wallet and signing operations. 4. Preinstall, vendor, or distribute audited package artifacts through a controlled channel. 5. Verify package tarball and native-binary integrity against independently published cryptographic hashes. 6. Generate and review a software bill of materials for each release. 7. Monitor dependency ownership, release provenance, signatures, and vulnerability advisories. 8. Disable npm lifecycle scripts unless a reviewed dependency explicitly requires them. 9. Execute wallet tooling in a sandbox with restricted filesystem and network access. 10. Document and audit the native signing module's source code, build process, reproducibility, and handling of wallet shares. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (16)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The documented behavior understates important security-relevant facts: external signing/relay communication, fixed testnet assumptions, and materially unsafe swap semantics where slippage is accepted but apparently not enforced and amountOutMin is zero. For a trading skill, these mismatches can cause users or agents to approve and sign transactions under false assumptions, leading to sandwiching, severe price loss, or execution on the wrong network/path.

Missing User Warnings

High
Confidence
96% confidence
Finding
The skill documents token transfers and swaps without clear warnings that blockchain transactions are irreversible, may incur gas costs, and can send assets to unrecoverable addresses. In a wallet-integrated context, missing warnings materially increase the chance of user error leading to permanent asset loss.

Missing User Warnings

High
Confidence
98% confidence
Finding
Documenting unlimited approvals as the default when no amount is provided, without a strong safety warning, encourages a dangerous permission pattern. If the spender contract is malicious, compromised, upgraded unexpectedly, or later exploited, it can drain all approved tokens without further consent.

Missing User Warnings

High
Confidence
98% confidence
Finding
The skill exposes arbitrary contract-call capability with signing and optional value transfer, but does not present it as a high-risk primitive. This effectively allows any calldata to be sent to any contract, enabling approvals, asset transfers, proxy execution, malicious interactions, or irreversible loss if invoked incorrectly or through prompt manipulation.

Missing User Warnings

High
Confidence
98% confidence
Finding
The `approve` command defaults to `2^256 - 1` unlimited allowance when no amount is provided, without a strong warning or confirmation step. Unlimited approvals are a common precursor to wallet drains if the spender is compromised, malicious, upgraded, or incorrectly specified, because they grant ongoing transfer rights over the token balance.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
General-purpose smart contract execution is unjustified by the skill's stated purpose and gives the agent authority to perform arbitrary onchain actions with MPC-signed transactions. Because the same signing path is used for normal wallet operations, a user invoking a seemingly trading-related skill could be exposed to full-wallet transaction execution beyond expected boundaries.

Missing User Warnings

High
Confidence
99% confidence
Finding
The raw contract-call path signs arbitrary calldata with no decoding, no semantic validation, and no explicit safety interstitial beyond a generic log line. In the context of an MPC wallet skill, this enables opaque asset-draining actions such as approvals, arbitrary token transfers, permit flows, or malicious contract interactions that the user may not understand.

Known Vulnerable Dependency: ws==8.18.3 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
97% confidence
Finding
The lockfile pins ws to 8.18.3, and the finding cites known vulnerabilities affecting that exact version: uninitialized memory disclosure and memory-exhaustion denial of service. In this skill’s context, WebSocket handling may be reachable through blockchain RPC or streaming interactions via viem, so a vulnerable transitive dependency can expose the agent process to remote attacks or instability during DeFi/trading operations.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill invokes external CLI commands that inherently use network access and environment-based configuration, but it does not declare an explicit tool scope or permission boundary. In an agent setting, this can cause the skill to be invoked with broader execution privileges than users or orchestrators expect, increasing the chance of unintended network calls or secret-bearing environment use.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrases are broad enough to activate high-risk blockchain transaction tooling for generic mentions of trading, DEX use, token balance, or contract calls. In an autonomous or assistive agent, overbroad activation can route benign user requests into a skill capable of signing irreversible on-chain actions.

Dynamic Request Target

Medium
Category
Server-Side Request Forgery
Content
const hash = keccak256(unsignedSerializedTx);
  const hashBytes = Buffer.from(hash.slice(2), 'hex');

  const resp = await fetch(`https://${SIGN_URL}/agent/sign/ecdsa`, {
    method: 'POST',
    headers: { 'X-API-KEY': config.apiKey },
    body: JSON.stringify({ hash: Buffer.from(hashBytes).toString('hex') }),
Confidence
60% confidence
Finding
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The `contract-call` command allows signing and broadcasting arbitrary calldata to any address, which materially exceeds the advertised trading-focused scope. In a wallet-signing skill, this effectively exposes a general-purpose transaction executor that can approve malicious spenders, transfer assets, interact with unsafe contracts, or alter protocol positions under the wallet's authority.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The `swap` command accepts a `--slippage` option but computes `slippageBps` without ever using it; all swaps pass `amountOutMin` as `0n`. This means transactions have no effective price protection and can execute at any output amount, exposing users to severe MEV, sandwich attacks, and unexpectedly bad execution.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "files": ["index.js"],
  "dependencies": {
    "goodwallet": "^0.2.0",
    "viem": "^2.28.0",
    "commander": "^13.1.0"
  },
Confidence
87% confidence
Finding
The dependency uses a caret range, which allows new upstream releases within the same major version to be installed without explicit review. In a wallet/trading skill that can sign blockchain transactions, a compromised or malicious dependency update could directly affect transaction construction, approvals, or private signing workflows, making supply-chain risk more consequential than in ordinary apps.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"files": ["index.js"],
  "dependencies": {
    "goodwallet": "^0.2.0",
    "viem": "^2.28.0",
    "commander": "^13.1.0"
  },
  "keywords": ["mpc", "wallet", "trading", "erc20", "defi", "agentic"],
Confidence
84% confidence
Finding
Using an unpinned viem version permits automatic uptake of future compatible releases that may introduce malicious code, regressions, or unsafe transaction/ABI handling. Because this skill performs DeFi and contract interactions, any supply-chain compromise in a core blockchain library could alter destinations, calldata, balances, or swap behavior with direct financial impact.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "goodwallet": "^0.2.0",
    "viem": "^2.28.0",
    "commander": "^13.1.0"
  },
  "keywords": ["mpc", "wallet", "trading", "erc20", "defi", "agentic"],
  "license": "UNLICENSED"
Confidence
80% confidence
Finding
The commander dependency is also unpinned, so upstream changes can be pulled in unexpectedly. While a CLI framework is less security-critical than wallet or blockchain libraries, it still expands the attack surface for supply-chain attacks, especially in tooling that may be used to trigger sensitive wallet actions.

Static analysis

Detected: suspicious.env_credential_access, suspicious.secret_argv_exposure

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
index.js:110

Instructions pass high-value credentials through process argv.

Critical
Code
suspicious.secret_argv_exposure
Location
SKILL.md:40