Skip to content

Commit

Permalink
feat: improve ABI Coders decode validation (#1426)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Jan 5, 2024
1 parent ad7ee46 commit 78e5e40
Show file tree
Hide file tree
Showing 34 changed files with 586 additions and 123 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-pumpkins-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/abi-coder": minor
---

Improve decode validation of ABI Coders
5 changes: 0 additions & 5 deletions packages/abi-coder/src/coders/abstract-coder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FuelError, type ErrorCode } from '@fuel-ts/errors';
import type { BN } from '@fuel-ts/math';
import type { BytesLike } from 'ethers';

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

throwError(errorCode: ErrorCode, message: string): never {
throw new FuelError(errorCode, message);
}

abstract encode(value: TInput, length?: number): Uint8Array;

abstract decode(data: Uint8Array, offset: number, length?: number): [TDecoded, number];
Expand Down
32 changes: 31 additions & 1 deletion packages/abi-coder/src/coders/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FuelError, ErrorCode } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';

import { U8_MAX } from '../../test/utils/constants';
import { U32_MAX, U8_MAX } from '../../test/utils/constants';

import type { SmallBytesOptions } from './abstract-coder';
import { ArrayCoder } from './array';
Expand Down Expand Up @@ -108,4 +108,34 @@ describe('ArrayCoder', () => {
new FuelError(ErrorCode.ENCODE_ERROR, 'Types/values length mismatch.')
);
});

it('throws when decoding empty bytes', async () => {
const coder = new ArrayCoder(new NumberCoder('u8'), 1);
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
);
});

it('throws when decoding invalid bytes (too small)', async () => {
const coder = new ArrayCoder(new NumberCoder('u8'), 8);
const input = new Uint8Array([0]);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
);
});

it('throws when decoding vec larger than max size', async () => {
const coder = new ArrayCoder(new NumberCoder('u8'), 8);
const input = new Uint8Array(U32_MAX + 1);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid array data size.')
);
});
});
11 changes: 8 additions & 3 deletions packages/abi-coder/src/coders/array.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ErrorCode } from '@fuel-ts/errors';
import { ErrorCode, FuelError } from '@fuel-ts/errors';

import { MAX_BYTES } from '../constants';
import { concatWithDynamicData } from '../utilities';

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

encode(value: InputValueOf<TCoder>): Uint8Array {
if (!Array.isArray(value)) {
this.throwError(ErrorCode.ENCODE_ERROR, `Expected array value.`);
throw new FuelError(ErrorCode.ENCODE_ERROR, `Expected array value.`);
}

if (this.length !== value.length) {
this.throwError(ErrorCode.ENCODE_ERROR, `Types/values length mismatch.`);
throw new FuelError(ErrorCode.ENCODE_ERROR, `Types/values length mismatch.`);
}

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

decode(data: Uint8Array, offset: number): [DecodedValueOf<TCoder>, number] {
if (data.length < this.encodedLength || data.length > MAX_BYTES) {
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid array data size.`);
}

let newOffset = offset;
const decodedValue = Array(this.length)
.fill(0)
Expand Down
70 changes: 44 additions & 26 deletions packages/abi-coder/src/coders/b256.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';

import { B256Coder } from './b256';

/**
Expand Down Expand Up @@ -46,52 +49,67 @@ describe('B256Coder', () => {
expect(actualLength).toBe(expectedLength);
});

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

expect(() => {
coder.encode(invalidInput);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.encode(invalidInput),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

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

expect(() => {
coder.decode(invalidInput, 0);
}).toThrow();
await expectToThrowFuelError(
() => coder.decode(invalidInput, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data size.')
);
});

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

expect(() => {
coder.encode(invalidInput);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.encode(invalidInput),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

it('should throw an error when encoding a 512 bit hash string', () => {
it('should throw an error when encoding a 512 bit hash string', async () => {
const B512 =
'0x8e9dda6f7793745ac5aacf9e907cae30b2a01fdf0d23b7750a85c6a44fca0c29f0906f9d1f1e92e6a1fb3c3dcef3cc3b3cdbaae27e47b9d9a4c6a4fce4cf16b2';

expect(() => {
coder.encode(B512);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.encode(B512),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

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

expect(() => {
coder.decode(invalidInput, 1);
}).toThrow('Invalid size for b256');
await expectToThrowFuelError(
() => coder.encode(invalidInput),
new FuelError(ErrorCode.ENCODE_ERROR, 'Invalid b256.')
);
});

it('should throw an error when encoding a 256 bit hash string that is not a hex string', () => {
const invalidInput = 'not a hex string';
it('throws when decoding empty bytes', async () => {
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 data size.')
);
});

it('should throw an error when decoding an encoded b256 bit hash string that is too long', async () => {
const invalidInput = new Uint8Array(Array.from(Array(65).keys()));

expect(() => {
coder.encode(invalidInput);
}).toThrow('Invalid b256');
await expectToThrowFuelError(
() => coder.decode(invalidInput, 62),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b256 byte data size.')
);
});
});
25 changes: 17 additions & 8 deletions packages/abi-coder/src/coders/b256.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
import { ErrorCode } from '@fuel-ts/errors';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { bn, toHex } from '@fuel-ts/math';
import { getBytesCopy } from 'ethers';

import { WORD_SIZE } from '../constants';

import { Coder } from './abstract-coder';

export class B256Coder extends Coder<string, string> {
constructor() {
super('b256', 'b256', 32);
super('b256', 'b256', WORD_SIZE * 4);
}

encode(value: string): Uint8Array {
let encodedValue;
try {
encodedValue = getBytesCopy(value);
} catch (error) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
if (encodedValue.length !== 32) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
if (encodedValue.length !== this.encodedLength) {
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
return encodedValue;
}

decode(data: Uint8Array, offset: number): [string, number] {
let bytes = data.slice(offset, offset + 32);
if (data.length < this.encodedLength) {
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b256 data size.`);
}

let bytes = data.slice(offset, offset + this.encodedLength);

const decoded = bn(bytes);
if (decoded.isZero()) {
bytes = new Uint8Array(32);
}
if (bytes.length !== 32) {
this.throwError(ErrorCode.DECODE_ERROR, `'Invalid size for b256'.`);

if (bytes.length !== this.encodedLength) {
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b256 byte data size.`);
}

return [toHex(bytes, 32), offset + 32];
}
}
46 changes: 30 additions & 16 deletions packages/abi-coder/src/coders/b512.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { expectToThrowFuelError } from '@fuel-ts/errors/test-utils';

import { B512Coder } from './b512';

/**
Expand Down Expand Up @@ -58,14 +61,6 @@ describe('B512Coder', () => {
}).toThrow(/Invalid struct B512/);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too short', () => {
const invalidInput = B512_ENCODED.slice(0, B512_ENCODED.length - 1);

expect(() => {
coder.decode(invalidInput, 0);
}).toThrow('Invalid size for b512');
});

it('should throw an error when encoding a 512 bit hash string that is too long', () => {
const invalidInput = `${B512_DECODED}0`;

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

it('should throw an error when decoding an encoded 512 bit hash string that is too long', () => {
const invalidInput = new Uint8Array(Array.from(Array(32).keys()));

expect(() => {
coder.decode(invalidInput, 1);
}).toThrow('Invalid size for b512');
});

it('should throw an error when encoding a 512 bit hash string that is not a hex string', () => {
const invalidInput = 'not a hex string';

expect(() => {
coder.encode(invalidInput);
}).toThrow(/Invalid struct B512/);
});

it('throws when decoding empty bytes', async () => {
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data size.')
);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too short', async () => {
const invalidInput = B512_ENCODED.slice(0, B512_ENCODED.length - 1);

await expectToThrowFuelError(
() => coder.decode(invalidInput, 8),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 data size.')
);
});

it('should throw an error when decoding an encoded 512 bit hash string that is too long', async () => {
const invalidInput = new Uint8Array(Array.from(Array(65).keys()));

await expectToThrowFuelError(
() => coder.decode(invalidInput, 8),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid b512 byte data size.')
);
});
});
27 changes: 18 additions & 9 deletions packages/abi-coder/src/coders/b512.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,45 @@
import { ErrorCode } from '@fuel-ts/errors';
import { ErrorCode, FuelError } from '@fuel-ts/errors';
import { bn, toHex } from '@fuel-ts/math';
import { getBytesCopy } from 'ethers';

import { WORD_SIZE } from '../constants';

import { Coder } from './abstract-coder';

export class B512Coder extends Coder<string, string> {
constructor() {
super('b512', 'struct B512', 64);
super('b512', 'struct B512', WORD_SIZE * 8);
}

encode(value: string): Uint8Array {
let encodedValue;
try {
encodedValue = getBytesCopy(value);
} catch (error) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
if (encodedValue.length !== 64) {
this.throwError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
if (encodedValue.length !== this.encodedLength) {
throw new FuelError(ErrorCode.ENCODE_ERROR, `Invalid ${this.type}.`);
}
return encodedValue;
}

decode(data: Uint8Array, offset: number): [string, number] {
let bytes = data.slice(offset, offset + 64);
if (data.length < this.encodedLength) {
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b512 data size.`);
}

let bytes = data.slice(offset, offset + this.encodedLength);

const decoded = bn(bytes);
if (decoded.isZero()) {
bytes = new Uint8Array(64);
}
if (bytes.length !== 64) {
this.throwError(ErrorCode.DECODE_ERROR, `Invalid size for b512.`);

if (bytes.length !== this.encodedLength) {
throw new FuelError(ErrorCode.DECODE_ERROR, `Invalid b512 byte data size.`);
}
return [toHex(bytes, 64), offset + 64];

return [toHex(bytes, this.encodedLength), offset + this.encodedLength];
}
}
9 changes: 9 additions & 0 deletions packages/abi-coder/src/coders/boolean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,13 @@ describe('BooleanCoder', () => {
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid boolean value.')
);
});

it('throws when decoding empty bytes', async () => {
const input = new Uint8Array(0);

await expectToThrowFuelError(
() => coder.decode(input, 0),
new FuelError(ErrorCode.DECODE_ERROR, 'Invalid boolean data size.')
);
});
});
Loading

0 comments on commit 78e5e40

Please sign in to comment.