T09 · Insecure Skill Coding Practices
- Location
skill.py:547- Finding
RPC Failover Can Cause Duplicate Blockchain Payments
- Content
View full analysis
Vulnerability Details
File Location:
skill.py:547-576andskill.py:616-652
Vulnerability Type: Unsafe transaction retry following an uncertain broadcast result
Risk Level: HighVulnerable Code
ETH payment implementation:
python # Try primary RPC first, then fallback on first failure for idx, rpc_url in enumerate(self.rpc_endpoints): try: w3 = Web3(Web3.HTTPProvider(rpc_url)) nonce = w3.eth.get_transaction_count(account.address, "pending") gas_price = w3.eth.gas_price tx = { "from": account.address, "to": Web3.to_checksum_address(to_address), "value": w3.to_wei(amount_eth, "ether"), "gasPrice": gas_price, "nonce": nonce, "chainId": 1 } tx["gas"] = w3.eth.estimate_gas(tx) signed = w3.eth.account.sign_transaction(tx, private_key) tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction) receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180) return { "success": receipt.status == 1, "tx_hash": tx_hash.hex(), "from": account.address, "to": to_address, "amount_eth": amount_eth, "gas_used": receipt.gasUsed, "block_number": receipt.blockNumber, "rpc_used": rpc_url, "rpc_failover_used": idx > 0, } except Exception as e: last_error = f"{rpc_url}: {e}" continueERC-20 payment implementation:
python for idx, rpc_url in enumerate(self.rpc_endpoints): try: w3 = Web3(Web3.HTTPProvider(rpc_url)) token = w3.eth.contract( address=Web3.to_checksum_address(token_address), abi=erc20_abi ) amount_wei = int(amount * (10 ** decimals)) nonce = w3.eth.get_transaction_count(accoun ...[truncated 3870 chars]- Remediation
View remediation
Remediation Suggestions
- Separate transaction phases and error handling. Distinguish errors occurring before broadcast from errors occurring after a transaction hash has been obtained.
- Never create a new payment automatically after broadcast uncertainty. Once
send_raw_transactionsucceeds, preserve the transaction hash and return apendingorunknownstatus if receipt polling fails. - Poll the original hash through fallback providers. Fallback RPC endpoints should call receipt and transaction lookup methods using the original transaction hash rather than create another transfer.
- Rebroadcast only identical signed bytes. If propagation must be retried, submit the exact same signed raw transaction. Reusing the same nonce and payload ensures the retry represents the same logical transaction rather than another payment.
- Persist reconciliation data. Record the transaction hash, sender, recipient, value, nonce, chain ID, and signed-transaction identifier before waiting for confirmation.
- Require explicit user confirmation for replacement transactions. If the original transaction is conclusively dropped and a replacement is necessary, present its status and require approval before signing another transfer.
- Add transaction-state checks. Check the original hash across all configured providers and inspect the account nonce before deciding that a transaction was never accepted.
- Add failure-mode tests. Cover successful broadcast followed by receipt timeout, provider disagreement, delayed propagation, process restart, and an original transaction confirming after fallback begins.
- Use deterministic monetary types. As an additional payment-safety measure, accept decimal strings or
Decimalvalues instead of floating-point amounts and convert them deterministically into base units before signing.
