T09 · Insecure Skill Coding Practices
- Location
SKILL.md:692- Finding
Shared Transaction Wrapper Does Not Enforce the Required Confirmation Gate
- Content
View full analysis
Vulnerability Details
File Location:
SKILL.md:659,SKILL.md:692-718; affected write examples includeSKILL.md:242-243,263,273,321-322,389,401-402,432,466,482,486, and505-527
Vulnerability Type: Missing authorization check before blockchain transaction broadcast
Risk Level: HighVulnerable Code
The setup declares a confirmation flag:
typescript const CONFIRM = process.argv.includes("--confirm"); // require --confirm to broadcastHowever, the shared transaction wrapper does not inspect that flag:
typescript async function sendWithRetry( txBuilder: TransactionBuilder, conn: Connection, computeUnits = 400_000, ): Promise<string> { const microLamports = await recommendedPriorityFee(conn); txBuilder.prependInstruction({ instructions: [ ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits }), ComputeBudgetProgram.setComputeUnitPrice({ microLamports }), ], cleanupInstructions: [], signers: [], }); for (let attempt = 1; attempt <= 3; attempt++) { let sig: string; try { sig = await txBuilder.buildAndExecute(); } catch (e: any) { if (attempt < 3 && /Blockhash|expired|BlockheightExceeded/i.test(e.message)) continue; throw e; } for (let i = 0; i < 30; i++) { const status = (await conn.getSignatureStatus(sig)).value; if (status?.err) throw new Error(`Tx ${sig} failed: ${JSON.stringify(status.err)}`); if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") return sig; await new Promise(r => setTimeout(r, 1000)); } throw new Error(`Tx ${sig} did not confirm within 30s`); } throw new Error("unreachable"); }Write playbooks call the wrapper directly, for example:
typescript const tx = await pool.swap(quote); const sig = aw ...[truncated 2023 chars]- Remediation
View remediation
Remediation Suggestions
Enforce confirmation at the single transaction-broadcast boundary rather than relying on every caller:
typescript async function sendWithRetry( txBuilder: TransactionBuilder, conn: Connection, confirm: boolean, computeUnits = 400_000, ): Promise<string> { if (!confirm) { throw new Error("Dry run only. Pass --confirm to broadcast."); } // Existing send logic follows. }Every call should pass
CONFIRMexplicitly:typescript const sig = await sendWithRetry(tx, connection, CONFIRM);Additional hardening should include:
- Separate preview and execution commands so dry-run code cannot reach a send API.
- Simulate transactions before approval and print pool, mint, position, expected amounts, minimum outputs, fees, and slippage bounds.
- Require confirmation immediately before signing, after fresh pool data and quotes are obtained.
- Add automated tests that mock
buildAndExecute()and verify it is never called when confirmation is absent. - Apply the same centralized guard to raw
connection.sendTransaction()andsendAndConfirmTransaction()paths.
