Skip to content

Commit 1fbc077

Browse files
TX: fix TX decoding when some values are actually arrays (#2284)
* tx: fix decoding * Add tests for array inputs * Validate keys are valid tx data keys * tx: add more input value checks and fix the checker * Address feedback Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com>
1 parent a7bb5ba commit 1fbc077

File tree

5 files changed

+150
-2
lines changed

5 files changed

+150
-2
lines changed

packages/tx/src/baseTransaction.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,31 @@ export abstract class BaseTransaction<TransactionObject> {
451451
}
452452
}
453453

454+
protected static _validateNotArray(values: { [key: string]: any }) {
455+
const txDataKeys = [
456+
'nonce',
457+
'gasPrice',
458+
'gasLimit',
459+
'to',
460+
'value',
461+
'data',
462+
'v',
463+
'r',
464+
's',
465+
'type',
466+
'baseFee',
467+
'maxFeePerGas',
468+
'chainId',
469+
]
470+
for (const [key, value] of Object.entries(values)) {
471+
if (txDataKeys.includes(key)) {
472+
if (Array.isArray(value)) {
473+
throw new Error(`${key} cannot be an array`)
474+
}
475+
}
476+
}
477+
}
478+
454479
/**
455480
* Return a compact error string representation of the object
456481
*/

packages/tx/src/eip1559Transaction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<FeeMarketEIP155
117117
s,
118118
] = values
119119

120+
this._validateNotArray({ chainId, v })
120121
validateNoLeadingZeroes({ nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, value, v, r, s })
121122

122123
return new FeeMarketEIP1559Transaction(
@@ -174,6 +175,8 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction<FeeMarketEIP155
174175
maxPriorityFeePerGas: this.maxPriorityFeePerGas,
175176
})
176177

178+
BaseTransaction._validateNotArray(txData)
179+
177180
if (this.gasLimit * this.maxFeePerGas > MAX_INTEGER) {
178181
const msg = this._errorMsg('gasLimit * maxFeePerGas cannot exceed MAX_INTEGER (2^256-1)')
179182
throw new Error(msg)

packages/tx/src/eip2930Transaction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export class AccessListEIP2930Transaction extends BaseTransaction<AccessListEIP2
103103

104104
const [chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, v, r, s] = values
105105

106+
this._validateNotArray({ chainId, v })
106107
validateNoLeadingZeroes({ nonce, gasPrice, gasLimit, value, v, r, s })
107108

108109
const emptyAccessList: AccessList = []
@@ -158,6 +159,8 @@ export class AccessListEIP2930Transaction extends BaseTransaction<AccessListEIP2
158159
gasPrice: this.gasPrice,
159160
})
160161

162+
BaseTransaction._validateNotArray(txData)
163+
161164
if (this.gasPrice * this.gasLimit > MAX_INTEGER) {
162165
const msg = this._errorMsg('gasLimit * gasPrice cannot exceed MAX_INTEGER')
163166
throw new Error(msg)

packages/tx/src/legacyTransaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export class Transaction extends BaseTransaction<Transaction> {
116116
throw new Error(msg)
117117
}
118118
this._validateCannotExceedMaxInteger({ gasPrice: this.gasPrice })
119+
BaseTransaction._validateNotArray(txData)
119120

120121
if (this.common.gteHardfork('spuriousDragon')) {
121122
if (!this.isSigned()) {

packages/tx/test/inputValue.spec.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common'
22
import { Address, toBuffer } from '@ethereumjs/util'
33
import * as tape from 'tape'
44

5-
import { Transaction } from '../src'
6-
5+
import {
6+
AccessListEIP2930Transaction,
7+
FeeMarketEIP1559Transaction,
8+
Transaction,
9+
TransactionFactory,
10+
} from '../src'
11+
12+
import type {
13+
AccessListEIP2930ValuesArray,
14+
FeeMarketEIP1559ValuesArray,
15+
TxValuesArray,
16+
} from '../src'
717
import type { AddressLike, BigIntLike, BufferLike } from '@ethereumjs/util'
818

919
// @returns: Array with subtypes of the AddressLike type for a given address
@@ -136,3 +146,109 @@ tape('[Transaction Input Values]', function (t) {
136146
st.end()
137147
})
138148
})
149+
150+
tape('[Invalid Array Input values]', (t) => {
151+
const txTypes = [0x0, 0x1, 0x2]
152+
for (const signed of [false, true]) {
153+
for (const txType of txTypes) {
154+
let tx = TransactionFactory.fromTxData({ type: txType })
155+
if (signed) {
156+
tx = tx.sign(Buffer.from('42'.repeat(32), 'hex'))
157+
}
158+
const rawValues = tx.raw()
159+
for (let x = 0; x < rawValues.length; x++) {
160+
rawValues[x] = <any>[1, 2, 3]
161+
switch (txType) {
162+
case 0:
163+
t.throws(() => Transaction.fromValuesArray(rawValues as TxValuesArray))
164+
break
165+
case 1:
166+
t.throws(() =>
167+
AccessListEIP2930Transaction.fromValuesArray(
168+
rawValues as AccessListEIP2930ValuesArray
169+
)
170+
)
171+
break
172+
case 2:
173+
t.throws(() =>
174+
FeeMarketEIP1559Transaction.fromValuesArray(rawValues as FeeMarketEIP1559ValuesArray)
175+
)
176+
break
177+
}
178+
}
179+
}
180+
}
181+
t.end()
182+
})
183+
184+
tape('[Invalid Access Lists]', (t) => {
185+
const txTypes = [0x1, 0x2]
186+
const invalidAccessLists = [
187+
[[]], // does not have an address and does not have slots
188+
[[[], []]], // the address is an array
189+
[['0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae']], // there is no storage slot array
190+
[
191+
[
192+
'0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae',
193+
['0x0000000000000000000000000000000000000000000000000000000000000003', []],
194+
],
195+
], // one of the slots is an array
196+
[
197+
[
198+
'0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae',
199+
['0x0000000000000000000000000000000000000000000000000000000000000003'],
200+
'0xab',
201+
],
202+
], // extra field
203+
[
204+
'0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae',
205+
['0x0000000000000000000000000000000000000000000000000000000000000003'],
206+
], // account/slot needs to be encoded in a deeper array layer
207+
]
208+
for (const signed of [false, true]) {
209+
for (const txType of txTypes) {
210+
for (const invalidAccessListItem of invalidAccessLists) {
211+
let tx: any
212+
try {
213+
tx = TransactionFactory.fromTxData({
214+
type: txType,
215+
accessList: <any>invalidAccessListItem,
216+
})
217+
if (signed) {
218+
tx = tx.sign(Buffer.from('42'.repeat(32), 'hex'))
219+
}
220+
t.fail('did not fail on `fromTxData`')
221+
} catch (e: any) {
222+
t.pass('failed ok on decoding in `fromTxData`')
223+
tx = TransactionFactory.fromTxData({ type: txType })
224+
if (signed) {
225+
tx = tx.sign(Buffer.from('42'.repeat(32), 'hex'))
226+
}
227+
}
228+
const rawValues = tx!.raw()
229+
230+
if (txType === 1 && rawValues[7].length === 0) {
231+
rawValues[7] = invalidAccessListItem
232+
} else if (txType === 2 && rawValues[8].length === 0) {
233+
rawValues[8] = invalidAccessListItem
234+
}
235+
236+
switch (txType) {
237+
case 1:
238+
t.throws(() =>
239+
AccessListEIP2930Transaction.fromValuesArray(
240+
rawValues as AccessListEIP2930ValuesArray
241+
)
242+
)
243+
break
244+
case 2:
245+
t.throws(() =>
246+
FeeMarketEIP1559Transaction.fromValuesArray(rawValues as FeeMarketEIP1559ValuesArray)
247+
)
248+
break
249+
}
250+
}
251+
}
252+
}
253+
t.end()
254+
})

0 commit comments

Comments
 (0)