Skip to content

Commit 78e5e40

Browse files
danielbatearboleyanedsalk
authored
feat: improve ABI Coders decode validation (FuelLabs#1426)
* feat: add bytes size valiadtion for overflow/undeflow and division issues * chore: changeset * feat: validate array and vector max size * chore: linting * feat: add legnth checks to vecs and arrays * feat: missing test coverage for enum and b256 coder: * test: mock calc fee logic in transaction summary test * chore: refactor * chore: further refactor * chore: rebuild * chore: rebuild * chore: remove redundant import Co-authored-by: Anderson Arboleya <[email protected]> * chore: remove redundant import Co-authored-by: Anderson Arboleya <[email protected]> * chore: linting * chore: update changeset Co-authored-by: Nedim Salkić <[email protected]> * refactor: remove throw error function from abstract coder --------- Co-authored-by: Anderson Arboleya <[email protected]> Co-authored-by: Nedim Salkić <[email protected]>
1 parent ad7ee46 commit 78e5e40

34 files changed

+586
-123
lines changed

.changeset/giant-pumpkins-refuse.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@fuel-ts/abi-coder": minor
3+
---
4+
5+
Improve decode validation of ABI Coders

packages/abi-coder/src/coders/abstract-coder.ts

-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FuelError, type ErrorCode } from '@fuel-ts/errors';
21
import type { BN } from '@fuel-ts/math';
32
import type { BytesLike } from 'ethers';
43

@@ -88,10 +87,6 @@ export abstract class Coder<TInput = unknown, TDecoded = unknown> {
8887
this.encodedLength = encodedLength;
8988
}
9089

91-
throwError(errorCode: ErrorCode, message: string): never {
92-
throw new FuelError(errorCode, message);
93-
}
94-
9590
abstract encode(value: TInput, length?: number): Uint8Array;
9691

9792
abstract decode(data: Uint8Array, offset: number, length?: number): [TDecoded, number];

packages/abi-coder/src/coders/array.test.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FuelError, ErrorCode } from '@fuel-ts/errors';
22
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
33

4-
import { U8_MAX } from '../../test/utils/constants';
4+
import { U32_MAX, U8_MAX } from '../../test/utils/constants';
55

66
import type { SmallBytesOptions } from './abstract-coder';
77
import { ArrayCoder } from './array';
@@ -108,4 +108,34 @@ describe('ArrayCoder', () => {
108108
new FuelError(ErrorCode.ENCODE_ERROR, 'Types/values length mismatch.')
109109
);
110110
});
111+
112+
it('throws when decoding empty bytes', async () => {
113+
const coder = new ArrayCoder(new NumberCoder('u8'), 1);
114+
const input = new Uint8Array(0);
115+
116+
await expectToThrowFuelError(
117+
() => coder.decode(input, 0),
118+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
119+
);
120+
});
121+
122+
it('throws when decoding invalid bytes (too small)', async () => {
123+
const coder = new ArrayCoder(new NumberCoder('u8'), 8);
124+
const input = new Uint8Array([0]);
125+
126+
await expectToThrowFuelError(
127+
() => coder.decode(input, 0),
128+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
129+
);
130+
});
131+
132+
it('throws when decoding vec larger than max size', async () => {
133+
const coder = new ArrayCoder(new NumberCoder('u8'), 8);
134+
const input = new Uint8Array(U32_MAX + 1);
135+
136+
await expectToThrowFuelError(
137+
() => coder.decode(input, 0),
138+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
139+
);
140+
});
111141
});

packages/abi-coder/src/coders/array.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { ErrorCode } from '@fuel-ts/errors';
1+
import { ErrorCode, FuelError } from '@fuel-ts/errors';
22

3+
import { MAX_BYTES } from '../constants';
34
import { concatWithDynamicData } from '../utilities';
45

56
import type { TypesOfCoder } from './abstract-coder';
@@ -23,17 +24,21 @@ export class ArrayCoder<TCoder extends Coder> extends Coder<
2324

2425
encode(value: InputValueOf<TCoder>): Uint8Array {
2526
if (!Array.isArray(value)) {
26-
this.throwError(ErrorCode.ENCODE_ERROR, `Expected array value.`);
27+
throw new FuelError(ErrorCode.ENCODE_ERROR, `Expected array value.`);
2728
}
2829

2930
if (this.length !== value.length) {
30-
this.throwError(ErrorCode.ENCODE_ERROR, `Types/values length mismatch.`);
31+
throw new FuelError(ErrorCode.ENCODE_ERROR, `Types/values length mismatch.`);
3132
}
3233

3334
return concatWithDynamicData(Array.from(value).map((v) => this.coder.encode(v)));
3435
}
3536

3637
decode(data: Uint8Array, offset: number): [DecodedValueOf<TCoder>, number] {
38+
if (data.length < this.encodedLength || data.length > MAX_BYTES) {
39+
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid array data size.`);
40+
}
41+
3742
let newOffset = offset;
3843
const decodedValue = Array(this.length)
3944
.fill(0)

packages/abi-coder/src/coders/b256.test.ts

+44-26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { ErrorCode, FuelError } from '@fuel-ts/errors';
2+
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
3+
14
import { B256Coder } from './b256';
25

36
/**
@@ -46,52 +49,67 @@ describe('B256Coder', () => {
4649
expect(actualLength).toBe(expectedLength);
4750
});
4851

49-
it('should throw an error when encoding a 256 bit hash string that is too short', () => {
52+
it('should throw an error when encoding a 256 bit hash string that is too short', async () => {
5053
const invalidInput = B256_DECODED.slice(0, B256_DECODED.length - 1);
5154

52-
expect(() => {
53-
coder.encode(invalidInput);
54-
}).toThrow('Invalid b256');
55+
await expectToThrowFuelError(
56+
() => coder.encode(invalidInput),
57+
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
58+
);
5559
});
5660

57-
it('should throw an error when decoding an encoded 256 bit hash string that is too short', () => {
61+
it('should throw an error when decoding an encoded 256 bit hash string that is too short', async () => {
5862
const invalidInput = B256_ENCODED.slice(0, B256_ENCODED.length - 1);
5963

60-
expect(() => {
61-
coder.decode(invalidInput, 0);
62-
}).toThrow();
64+
await expectToThrowFuelError(
65+
() => coder.decode(invalidInput, 0),
66+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data size.')
67+
);
6368
});
6469

65-
it('should throw an error when encoding a 256 bit hash string that is too long', () => {
70+
it('should throw an error when encoding a 256 bit hash string that is too long', async () => {
6671
const invalidInput = `${B256_DECODED}0`;
6772

68-
expect(() => {
69-
coder.encode(invalidInput);
70-
}).toThrow('Invalid b256');
73+
await expectToThrowFuelError(
74+
() => coder.encode(invalidInput),
75+
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
76+
);
7177
});
7278

73-
it('should throw an error when encoding a 512 bit hash string', () => {
79+
it('should throw an error when encoding a 512 bit hash string', async () => {
7480
const B512 =
7581
'0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2';
7682

77-
expect(() => {
78-
coder.encode(B512);
79-
}).toThrow('Invalid b256');
83+
await expectToThrowFuelError(
84+
() => coder.encode(B512),
85+
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
86+
);
8087
});
8188

82-
it('should throw an error when decoding an encoded 256 bit hash string that is too long', () => {
83-
const invalidInput = new Uint8Array(Array.from(Array(32).keys()));
89+
it('should throw an error when encoding a 256 bit hash string that is not a hex string', async () => {
90+
const invalidInput = 'not a hex string';
8491

85-
expect(() => {
86-
coder.decode(invalidInput, 1);
87-
}).toThrow('Invalid size for b256');
92+
await expectToThrowFuelError(
93+
() => coder.encode(invalidInput),
94+
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
95+
);
8896
});
8997

90-
it('should throw an error when encoding a 256 bit hash string that is not a hex string', () => {
91-
const invalidInput = 'not a hex string';
98+
it('throws when decoding empty bytes', async () => {
99+
const input = new Uint8Array(0);
100+
101+
await expectToThrowFuelError(
102+
() => coder.decode(input, 0),
103+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data size.')
104+
);
105+
});
106+
107+
it('should throw an error when decoding an encoded b256 bit hash string that is too long', async () => {
108+
const invalidInput = new Uint8Array(Array.from(Array(65).keys()));
92109

93-
expect(() => {
94-
coder.encode(invalidInput);
95-
}).toThrow('Invalid b256');
110+
await expectToThrowFuelError(
111+
() => coder.decode(invalidInput, 62),
112+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 byte data size.')
113+
);
96114
});
97115
});

packages/abi-coder/src/coders/b256.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
1-
import { ErrorCode } from '@fuel-ts/errors';
1+
import { ErrorCode, FuelError } from '@fuel-ts/errors';
22
import { bn, toHex } from '@fuel-ts/math';
33
import { getBytesCopy } from 'ethers';
44

5+
import { WORD_SIZE } from '../constants';
6+
57
import { Coder } from './abstract-coder';
68

79
export class B256Coder extends Coder<string, string> {
810
constructor() {
9-
super('b256', 'b256', 32);
11+
super('b256', 'b256', WORD_SIZE * 4);
1012
}
1113

1214
encode(value: string): Uint8Array {
1315
let encodedValue;
1416
try {
1517
encodedValue = getBytesCopy(value);
1618
} catch (error) {
17-
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
19+
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
1820
}
19-
if (encodedValue.length !== 32) {
20-
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
21+
if (encodedValue.length !== this.encodedLength) {
22+
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
2123
}
2224
return encodedValue;
2325
}
2426

2527
decode(data: Uint8Array, offset: number): [string, number] {
26-
let bytes = data.slice(offset, offset + 32);
28+
if (data.length < this.encodedLength) {
29+
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b256 data size.`);
30+
}
31+
32+
let bytes = data.slice(offset, offset + this.encodedLength);
33+
2734
const decoded = bn(bytes);
2835
if (decoded.isZero()) {
2936
bytes = new Uint8Array(32);
3037
}
31-
if (bytes.length !== 32) {
32-
this.throwError(ErrorCode.DECODE_ERROR, `'Invalid size for b256'.`);
38+
39+
if (bytes.length !== this.encodedLength) {
40+
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b256 byte data size.`);
3341
}
42+
3443
return [toHex(bytes, 32), offset + 32];
3544
}
3645
}

packages/abi-coder/src/coders/b512.test.ts

+30-16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { ErrorCode, FuelError } from '@fuel-ts/errors';
2+
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';
3+
14
import { B512Coder } from './b512';
25

36
/**
@@ -58,14 +61,6 @@ describe('B512Coder', () => {
5861
}).toThrow(/Invalid struct B512/);
5962
});
6063

61-
it('should throw an error when decoding an encoded 512 bit hash string that is too short', () => {
62-
const invalidInput = B512_ENCODED.slice(0, B512_ENCODED.length - 1);
63-
64-
expect(() => {
65-
coder.decode(invalidInput, 0);
66-
}).toThrow('Invalid size for b512');
67-
});
68-
6964
it('should throw an error when encoding a 512 bit hash string that is too long', () => {
7065
const invalidInput = `${B512_DECODED}0`;
7166

@@ -82,19 +77,38 @@ describe('B512Coder', () => {
8277
}).toThrow(/Invalid struct B512/);
8378
});
8479

85-
it('should throw an error when decoding an encoded 512 bit hash string that is too long', () => {
86-
const invalidInput = new Uint8Array(Array.from(Array(32).keys()));
87-
88-
expect(() => {
89-
coder.decode(invalidInput, 1);
90-
}).toThrow('Invalid size for b512');
91-
});
92-
9380
it('should throw an error when encoding a 512 bit hash string that is not a hex string', () => {
9481
const invalidInput = 'not a hex string';
9582

9683
expect(() => {
9784
coder.encode(invalidInput);
9885
}).toThrow(/Invalid struct B512/);
9986
});
87+
88+
it('throws when decoding empty bytes', async () => {
89+
const input = new Uint8Array(0);
90+
91+
await expectToThrowFuelError(
92+
() => coder.decode(input, 0),
93+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data size.')
94+
);
95+
});
96+
97+
it('should throw an error when decoding an encoded 512 bit hash string that is too short', async () => {
98+
const invalidInput = B512_ENCODED.slice(0, B512_ENCODED.length - 1);
99+
100+
await expectToThrowFuelError(
101+
() => coder.decode(invalidInput, 8),
102+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data size.')
103+
);
104+
});
105+
106+
it('should throw an error when decoding an encoded 512 bit hash string that is too long', async () => {
107+
const invalidInput = new Uint8Array(Array.from(Array(65).keys()));
108+
109+
await expectToThrowFuelError(
110+
() => coder.decode(invalidInput, 8),
111+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 byte data size.')
112+
);
113+
});
100114
});

packages/abi-coder/src/coders/b512.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
1-
import { ErrorCode } from '@fuel-ts/errors';
1+
import { ErrorCode, FuelError } from '@fuel-ts/errors';
22
import { bn, toHex } from '@fuel-ts/math';
33
import { getBytesCopy } from 'ethers';
44

5+
import { WORD_SIZE } from '../constants';
6+
57
import { Coder } from './abstract-coder';
68

79
export class B512Coder extends Coder<string, string> {
810
constructor() {
9-
super('b512', 'struct B512', 64);
11+
super('b512', 'struct B512', WORD_SIZE * 8);
1012
}
1113

1214
encode(value: string): Uint8Array {
1315
let encodedValue;
1416
try {
1517
encodedValue = getBytesCopy(value);
1618
} catch (error) {
17-
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
19+
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
1820
}
19-
if (encodedValue.length !== 64) {
20-
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
21+
if (encodedValue.length !== this.encodedLength) {
22+
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
2123
}
2224
return encodedValue;
2325
}
2426

2527
decode(data: Uint8Array, offset: number): [string, number] {
26-
let bytes = data.slice(offset, offset + 64);
28+
if (data.length < this.encodedLength) {
29+
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b512 data size.`);
30+
}
31+
32+
let bytes = data.slice(offset, offset + this.encodedLength);
33+
2734
const decoded = bn(bytes);
2835
if (decoded.isZero()) {
2936
bytes = new Uint8Array(64);
3037
}
31-
if (bytes.length !== 64) {
32-
this.throwError(ErrorCode.DECODE_ERROR, `Invalid size for b512.`);
38+
39+
if (bytes.length !== this.encodedLength) {
40+
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b512 byte data size.`);
3341
}
34-
return [toHex(bytes, 64), offset + 64];
42+
43+
return [toHex(bytes, this.encodedLength), offset + this.encodedLength];
3544
}
3645
}

packages/abi-coder/src/coders/boolean.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,13 @@ describe('BooleanCoder', () => {
7070
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid boolean value.')
7171
);
7272
});
73+
74+
it('throws when decoding empty bytes', async () => {
75+
const input = new Uint8Array(0);
76+
77+
await expectToThrowFuelError(
78+
() => coder.decode(input, 0),
79+
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid boolean data size.')
80+
);
81+
});
7382
});

0 commit comments

Comments
 (0)