T09 · Insecure Skill Coding Practices
Error
- Location
- src/index.js:170
- Finding
- Live Trading Risk Controls Are Not Bound to the Signing Account## Vulnerability Details **File Location**: `src/index.js:170, 219-234`; related code in `src/sizing.js:27-37` and `src/trader.js:228-286` **Vulnerability Type**: Account identity mismatch in financial risk controls **Risk Level**: High ### Vulnerable 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
- ## Remediation Suggestions 1. At live startup, derive the signer address from `HYPERLIQUID_PRIVATE_KEY` using `ethers.Wallet` and reject startup unless it exactly matches `HYPERLIQUID_WALLET_ADDRESS`. 2. Use the derived signer address—not a separately trusted environment value—for every balance, position, and exposure query. 3. Construct a single immutable signer context containing the private key and derived address, then pass that context through sizing and execution functions. 4. Add a defense-in-depth identity check inside `placeOrder()` so direct callers cannot bypass the startup validation. 5. 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. 6. Treat failure to derive the signer address, query its account state, or verify identity equality as fail-closed conditions. 7. 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. ```
