From b264a23e1bb3d11371771c37490b34f70790351e Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 11 Nov 2026 03:40:36 +0000 Subject: [PATCH] fix: reject `chainId` values that are not positive safe integers in transaction envelope asserts Amp-Thread-ID: https://ampcode.com/threads/T-01a1c85c-51cb-75e9-8269-b23f8649f1c8 Co-authored-by: jxom --- .changeset/tx-envelope-chain-id-assert.md | 5 +++ src/core/TxEnvelopeEip1559.ts | 5 ++- src/core/TxEnvelopeEip2930.ts | 5 ++- src/core/TxEnvelopeLegacy.ts | 5 ++- .../_test/TransactionEnvelopeEip1559.test.ts | 28 ++++++++++++ .../_test/TransactionEnvelopeEip2930.test.ts | 28 ++++++++++++ .../_test/TransactionEnvelopeLegacy.test.ts | 28 ++++++++++++ src/tempo/TxEnvelopeTempo.test.ts | 43 +++++++++++++++++++ src/tempo/TxEnvelopeTempo.ts | 5 ++- 9 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 .changeset/tx-envelope-chain-id-assert.md diff --git a/.changeset/tx-envelope-chain-id-assert.md b/.changeset/tx-envelope-chain-id-assert.md new file mode 100644 index 000000000..babf9a7ee --- /dev/null +++ b/.changeset/tx-envelope-chain-id-assert.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Fixed `TxEnvelopeLegacy.assert`, `TxEnvelopeEip2930.assert`, `TxEnvelopeEip1559.assert` and `TxEnvelopeTempo.assert` to throw `InvalidChainIdError` for a `chainId` that is not a positive safe integer. diff --git a/src/core/TxEnvelopeEip1559.ts b/src/core/TxEnvelopeEip1559.ts index 35ed409b6..fb8db9359 100644 --- a/src/core/TxEnvelopeEip1559.ts +++ b/src/core/TxEnvelopeEip1559.ts @@ -71,7 +71,10 @@ export type Type = typeof type */ export function assert(envelope: PartialBy) { const { chainId, maxPriorityFeePerGas, maxFeePerGas, to } = envelope - if (chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) if (to) Address.assert(to, { strict: false }) if (maxFeePerGas && BigInt(maxFeePerGas) > 2n ** 256n - 1n) diff --git a/src/core/TxEnvelopeEip2930.ts b/src/core/TxEnvelopeEip2930.ts index ddbf6169c..ebc67979c 100644 --- a/src/core/TxEnvelopeEip2930.ts +++ b/src/core/TxEnvelopeEip2930.ts @@ -69,7 +69,10 @@ export type Type = typeof type */ export function assert(envelope: PartialBy) { const { chainId, gasPrice, to } = envelope - if (chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) if (to) Address.assert(to, { strict: false }) if (gasPrice && BigInt(gasPrice) > 2n ** 256n - 1n) diff --git a/src/core/TxEnvelopeLegacy.ts b/src/core/TxEnvelopeLegacy.ts index 8a5261c64..80da316d6 100644 --- a/src/core/TxEnvelopeLegacy.ts +++ b/src/core/TxEnvelopeLegacy.ts @@ -68,7 +68,10 @@ export type Type = typeof type export function assert(envelope: PartialBy) { const { chainId, gasPrice, to } = envelope if (to) Address.assert(to, { strict: false }) - if (typeof chainId !== 'undefined' && chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) if (gasPrice && BigInt(gasPrice) > 2n ** 256n - 1n) throw new TransactionEnvelope.GasPriceTooHighError({ gasPrice }) diff --git a/src/core/_test/TransactionEnvelopeEip1559.test.ts b/src/core/_test/TransactionEnvelopeEip1559.test.ts index 250f7593d..837d9c418 100644 --- a/src/core/_test/TransactionEnvelopeEip1559.test.ts +++ b/src/core/_test/TransactionEnvelopeEip1559.test.ts @@ -47,6 +47,34 @@ describe('assert', () => { ) }) + test('invalid chainId (not a positive safe integer)', () => { + expect(() => + TxEnvelopeEip1559.assert({ chainId: Number.NaN }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: 1.5 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: Number.MAX_SAFE_INTEGER + 1 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: '1' as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeEip1559.assert({ chainId: 1n as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid address', () => { expect(() => TxEnvelopeEip1559.assert({ to: '0x123', chainId: 1 })) .toThrowErrorMatchingInlineSnapshot(` diff --git a/src/core/_test/TransactionEnvelopeEip2930.test.ts b/src/core/_test/TransactionEnvelopeEip2930.test.ts index 15baa6ada..5513c19f9 100644 --- a/src/core/_test/TransactionEnvelopeEip2930.test.ts +++ b/src/core/_test/TransactionEnvelopeEip2930.test.ts @@ -23,6 +23,34 @@ describe('assert', () => { ) }) + test('invalid chainId (not a positive safe integer)', () => { + expect(() => + TxEnvelopeEip2930.assert({ chainId: Number.NaN }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: 1.5 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: Number.MAX_SAFE_INTEGER + 1 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: '1' as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeEip2930.assert({ chainId: 1n as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid address', () => { expect(() => TxEnvelopeEip2930.assert({ to: '0x123', chainId: 1 })) .toThrowErrorMatchingInlineSnapshot(` diff --git a/src/core/_test/TransactionEnvelopeLegacy.test.ts b/src/core/_test/TransactionEnvelopeLegacy.test.ts index d2431360b..42e50f651 100644 --- a/src/core/_test/TransactionEnvelopeLegacy.test.ts +++ b/src/core/_test/TransactionEnvelopeLegacy.test.ts @@ -23,6 +23,34 @@ describe('assert', () => { ) }) + test('invalid chainId (not a positive safe integer)', () => { + expect(() => + TxEnvelopeLegacy.assert({ chainId: Number.NaN }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: 1.5 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: Number.MAX_SAFE_INTEGER + 1 }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: '1' as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeLegacy.assert({ chainId: 1n as never }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) + test('invalid address', () => { expect(() => TxEnvelopeLegacy.assert({ to: '0x123', chainId: 1 })) .toThrowErrorMatchingInlineSnapshot(` diff --git a/src/tempo/TxEnvelopeTempo.test.ts b/src/tempo/TxEnvelopeTempo.test.ts index e393b0629..16a1afd7a 100644 --- a/src/tempo/TxEnvelopeTempo.test.ts +++ b/src/tempo/TxEnvelopeTempo.test.ts @@ -129,6 +129,49 @@ describe('assert', () => { `[TransactionEnvelope.InvalidChainIdError: Chain ID "0" is invalid.]`, ) }) + + test('invalid chain id (not a positive safe integer)', () => { + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: Number.NaN, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "NaN" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: 1.5, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1.5" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: Number.MAX_SAFE_INTEGER + 1, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "9007199254740992" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: '1' as never, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + expect(() => + TxEnvelopeTempo.assert({ + calls: [{ to: '0x0000000000000000000000000000000000000000' }], + chainId: 1n as never, + }), + ).toThrowErrorMatchingInlineSnapshot( + `[TransactionEnvelope.InvalidChainIdError: Chain ID "1" is invalid.]`, + ) + }) }) describe('deserialize', () => { diff --git a/src/tempo/TxEnvelopeTempo.ts b/src/tempo/TxEnvelopeTempo.ts index 8ff1d63f5..de9cb2047 100644 --- a/src/tempo/TxEnvelopeTempo.ts +++ b/src/tempo/TxEnvelopeTempo.ts @@ -204,7 +204,10 @@ export function assert(envelope: PartialBy) { if (feeToken) Address.assert(feeToken, { strict: false }) // Validate chain ID - if (chainId <= 0) + if ( + typeof chainId !== 'undefined' && + (!Number.isSafeInteger(chainId) || chainId <= 0) + ) throw new TransactionEnvelope.InvalidChainIdError({ chainId }) // Validate max fee per gas