T09 · Insecure Skill Coding Practices
- Location
src/index.js:170- Finding
Live Trading Risk Controls Are Not Bound to the Signing Account
- Content
View full analysis
Vulnerability Details
File Location:
src/index.js:170, 219-234; related code insrc/sizing.js:27-37andsrc/trader.js:228-286
Vulnerability Type: Account identity mismatch in financial risk controls
Risk Level: HighVulnerable Code
The autonomous loop obtains the account used for position checks and position sizing from a configurable address:
js const walletAddress = process.env.HYPERLIQUID_WALLET_ADDRESS || '';js // Enforce max open positions (live only) if (!isPaper && walletAddress) { const openPos = await getPositions(walletAddress); if (openPos.length >= maxOpenPos) { console.log(` Skip ${signal.coin}: ${maxOpenPos} positions open`); continue; } } // Position sizing let sizeUsd; if (isPaper) { sizeUsd = Math.min(signal.confidence * kellyFraction * 10_000, maxPosUsd); } else { try { const { calculatePositionSize } = require('./sizing'); const sizing = await calculatePositionSize(signal.confidence, walletAddress); sizeUsd = sizing.sizeUsd; console.log(` Sizing: $${sizeUsd} (${sizing.kellySizePct}% Kelly of $${sizing.balance})`); } catch (err) { console.warn(` Sizing failed: ${err.message} -- aborting trade`); continue; } }The sizing module queries the balance of that supplied address:
js async function calculatePositionSize(confidence, walletAddress) { if (confidence <= 0 || confidence > 1) { throw new Error(`Invalid confidence value: ${confidence} (must be 0–1)`); } const maxPositionUsd = parseFloat(process.env.MAX_POSITION_SIZE_USD) || 500; const maxRiskPct = parseFloat(process.env.MAX_ACCOUNT_RISK_PCT) || 2; const kellyFraction = parseFloat(process.env.KELLY_FRACTION) || 0.25; // Fetch live balance — hard abort if this fails const balance = await getAccountBalance(walletAddress);However, the resulting order is authorized using an independently configured private key:
js async function placeOrder({ coin, isBu ...[truncated 4321 chars]- Remediation
View remediation
Remediation Suggestions
- At live startup, derive the signer address from
HYPERLIQUID_PRIVATE_KEYusingethers.Walletand reject startup unless it exactly matchesHYPERLIQUID_WALLET_ADDRESS. - Use the derived signer address—not a separately trusted environment value—for every balance, position, and exposure query.
- Construct a single immutable signer context containing the private key and derived address, then pass that context through sizing and execution functions.
- Add a defense-in-depth identity check inside
placeOrder()so direct callers cannot bypass the startup validation. - Before signing each order, query the signer account's current positions and balance and enforce
MAX_OPEN_POSITIONS,MAX_ACCOUNT_RISK_PCT, and the absolute position cap against that same identity. - Treat failure to derive the signer address, query its account state, or verify identity equality as fail-closed conditions.
- Add automated tests covering mismatched addresses, missing addresses, stale configuration, direct invocation of
placeOrder(), and signer-account risk-limit enforcement.
A suitable startup pattern is:
js const { Wallet } = require('ethers'); const privateKey = process.env.HYPERLIQUID_PRIVATE_KEY; const configuredAddress = process.env.HYPERLIQUID_WALLET_ADDRESS; const signerAddress = new Wallet(privateKey).address; if ( !configuredAddress || signerAddress.toLowerCase() !== configuredAddress.toLowerCase() ) { throw new Error( 'HYPERLIQUID_WALLET_ADDRESS does not match HYPERLIQUID_PRIVATE_KEY' ); } // Use signerAddress for all subsequent account-state queries.- At live startup, derive the signer address from
