Skip to content

Commit 25e4f68

Browse files
BitHighlanderclaude
andcommitted
fix(tron): keep TRC-20 amounts as strings through dApp sign path
decodeTronTx was holding the decoded TRC-20 amount as BigInt, then casting to Number before stashing it on DecodedTronTx.sunAmount and passing it to signTronViaRest + the approval event. Any 18-decimal token (most non-stablecoin TRC-20s) exceeds Number.MAX_SAFE_INTEGER in base units, so the value silently rounds before the vault sees it — firmware displays the wrong amount on-device and the signed tx spends the wrong amount on-chain. Changes: - DecodedTronTx.sunAmount (number) → amountRaw (decimal string). Renamed to signal "this is base units not native sun". - TransferContract decode emits String(v.amount); TRC-20 decode emits BigInt.toString(); contract-call emits String(callValue). All paths preserve precision. - signTronViaRest signature widened to `string | number | bigint`. Internal stringification uses bigint.toString() when applicable so nothing re-enters the Number casting path. - Approval event now carries amountRaw (not sun) so downstream UIs that display the raw-unit hint get the untruncated value. Native TRX amounts never overflow Number (max 90B TRX in sun = 9e16, fits in 2^53 comfortably), but the change makes that path uniform and the code easier to audit. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 613c2f3 commit 25e4f68

1 file changed

Lines changed: 22 additions & 10 deletions

File tree

chrome-extension/src/background/chains/tronHandler.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,11 @@ async function buildTronTransfer(from: string, to: string, sunAmount: number): P
186186
* The vault's handler emulates emuWrap for the device, so this call may block
187187
* until the user confirms on the KeepKey.
188188
*/
189-
async function signTronViaRest(rawDataHex: string, toAddress: string, sunAmount: number): Promise<string> {
189+
async function signTronViaRest(
190+
rawDataHex: string,
191+
toAddress: string,
192+
amountRaw: string | number | bigint,
193+
): Promise<string> {
190194
const apiKey = getApiKey();
191195
let resp: Response;
192196
try {
@@ -200,7 +204,10 @@ async function signTronViaRest(rawDataHex: string, toAddress: string, sunAmount:
200204
addressNList: TRON_ADDRESS_N,
201205
raw_tx: rawDataHex,
202206
to_address: toAddress,
203-
amount: String(sunAmount),
207+
// Stringify without going through Number — 18-decimal TRC-20
208+
// amounts exceed Number.MAX_SAFE_INTEGER in base units and
209+
// would silently round before hitting the vault.
210+
amount: typeof amountRaw === 'bigint' ? amountRaw.toString() : String(amountRaw),
204211
}),
205212
signal: AbortSignal.timeout(60000),
206213
});
@@ -274,7 +281,11 @@ interface DecodedTronTx {
274281
kind: 'trx-transfer' | 'trc20-transfer' | 'contract-call';
275282
ownerAddress: string; // base58
276283
toAddress: string; // base58 — recipient for transfers, contract address for generic calls
277-
sunAmount: number; // native TRX in sun, TRC20 in token base units, contract-call in call_value (TRX)
284+
// Raw base-units as a DECIMAL STRING so 18-decimal TRC-20 amounts
285+
// survive the trip to the vault. Going through Number would truncate
286+
// at ~2^53 base units (9e15 — fine for 6-decimal TRX/USDT, broken
287+
// for 18-decimal tokens).
288+
amountRaw: string;
278289
displayAmount: string; // human-readable
279290
contractAddress?: string; // base58, non-native
280291
functionSelector?: string; // hex, contract-call only
@@ -312,7 +323,7 @@ async function decodeTronTx(tx: any): Promise<DecodedTronTx> {
312323
kind: 'trx-transfer',
313324
ownerAddress: await normalizeAddr(v.owner_address),
314325
toAddress: await normalizeAddr(v.to_address),
315-
sunAmount: v.amount,
326+
amountRaw: String(v.amount),
316327
displayAmount: String(v.amount / 1_000_000),
317328
};
318329
}
@@ -340,7 +351,7 @@ async function decodeTronTx(tx: any): Promise<DecodedTronTx> {
340351
kind: 'trc20-transfer',
341352
ownerAddress,
342353
toAddress: recipientBase58,
343-
sunAmount: Number(amount),
354+
amountRaw: amount.toString(),
344355
displayAmount: amount.toString(),
345356
contractAddress,
346357
};
@@ -349,14 +360,14 @@ async function decodeTronTx(tx: any): Promise<DecodedTronTx> {
349360
// Generic contract call (swaps, stake, approve, etc.). The firmware
350361
// parses the raw_data itself and signs based on what it finds; our
351362
// hints here are purely for the side-panel approval UI. Route
352-
// `call_value` (TRX attached to the call) to sunAmount so swaps that
353-
// spend native TRX display the right outgoing amount.
363+
// `call_value` (TRX attached to the call) to amountRaw so swaps
364+
// that spend native TRX display the right outgoing amount.
354365
const callValue = typeof v.call_value === 'number' ? v.call_value : 0;
355366
return {
356367
kind: 'contract-call',
357368
ownerAddress,
358369
toAddress: contractAddress,
359-
sunAmount: callValue,
370+
amountRaw: String(callValue),
360371
displayAmount: String(callValue / 1_000_000),
361372
contractAddress,
362373
functionSelector: selector,
@@ -455,7 +466,8 @@ export const handleTronRequest = async (
455466
from: sender,
456467
to: decoded.toAddress,
457468
amount: decoded.displayAmount,
458-
sun: decoded.sunAmount,
469+
// String, not number — preserves precision for 18-decimal TRC-20.
470+
amountRaw: decoded.amountRaw,
459471
contractAddress: decoded.contractAddress,
460472
functionSelector: decoded.functionSelector,
461473
tronGridTx: tx,
@@ -471,7 +483,7 @@ export const handleTronRequest = async (
471483
throw createProviderRpcError(4001, 'User denied transaction');
472484
}
473485

474-
const signatureHex = await signTronViaRest(tx.raw_data_hex, decoded.toAddress, decoded.sunAmount);
486+
const signatureHex = await signTronViaRest(tx.raw_data_hex, decoded.toAddress, decoded.amountRaw);
475487

476488
// Preserve any existing `signature` array from the dApp (multi-sig
477489
// case) and append ours; most dApps pass in an unsigned tx so this

0 commit comments

Comments
 (0)