packages/codec/lib/abi-data/decode/index.ts
/**
* @protected
*
* @packageDocumentation
*/
import debugModule from "debug";
const debug = debugModule("codec:abi-data:decode");
import type BN from "bn.js";
import read from "@truffle/codec/read";
import * as Conversion from "@truffle/codec/conversion";
import * as Basic from "@truffle/codec/basic";
import * as Bytes from "@truffle/codec/bytes";
import * as Format from "@truffle/codec/format";
import type * as Pointer from "@truffle/codec/pointer";
import type { DecoderRequest, DecoderOptions } from "@truffle/codec/types";
import * as Evm from "@truffle/codec/evm";
import { abiSizeInfo } from "@truffle/codec/abi-data/allocate";
import { handleDecodingError, StopDecodingError } from "@truffle/codec/errors";
type AbiLocation = "calldata" | "eventdata" | "returndata"; //leaving out "abi" as it shouldn't occur here
export function* decodeAbi(
dataType: Format.Types.Type,
pointer: Pointer.AbiDataPointer,
info: Evm.EvmInfo,
options: DecoderOptions = {}
): Generator<DecoderRequest, Format.Values.Result, Uint8Array | null> {
if (
Format.Types.isReferenceType(dataType) ||
dataType.typeClass === "tuple"
) {
//I don't want tuples to be considered a reference type, but it makes sense
//to group them for this purpose
let dynamic: boolean;
try {
dynamic = abiSizeInfo(dataType, info.allocations.abi).dynamic;
} catch (error) {
return handleDecodingError(dataType, error, options.strictAbiMode);
}
if (dynamic) {
return yield* decodeAbiReferenceByAddress(
dataType,
pointer,
info,
options
);
} else {
return yield* decodeAbiReferenceStatic(dataType, pointer, info, options);
}
} else {
debug("pointer %o", pointer);
return yield* Basic.Decode.decodeBasic(dataType, pointer, info, options);
}
}
export function* decodeAbiReferenceByAddress(
dataType: Format.Types.ReferenceType | Format.Types.TupleType,
pointer: Pointer.AbiDataPointer | Pointer.StackFormPointer,
info: Evm.EvmInfo,
options: DecoderOptions = {}
): Generator<DecoderRequest, Format.Values.Result, Uint8Array | null> {
let { strictAbiMode: strict, abiPointerBase: base, lengthOverride } = options;
base = base || 0; //in case base was undefined
const {
allocations: { abi: allocations },
state
} = info;
debug("pointer %o", pointer);
//this variable holds the location we should look to *next*
//stack pointers point to calldata; other pointers point to same location
const location: AbiLocation =
pointer.location === "stack" || pointer.location === "stackliteral"
? "calldata"
: pointer.location;
if (pointer.location !== "stack" && pointer.location !== "stackliteral") {
//length overrides are only applicable when you're decoding a pointer
//from the stack! otherwise they must be ignored!
lengthOverride = undefined;
}
let rawValue: Uint8Array;
try {
rawValue = yield* read(pointer, state);
} catch (error) {
return handleDecodingError(dataType, error, strict);
}
let rawValueAsBN = Conversion.toBN(rawValue);
debug("rawValue: %O", rawValue);
debug("rawValueAsBN: %O", rawValueAsBN);
let rawValueAsNumber: number;
try {
rawValueAsNumber = rawValueAsBN.toNumber();
} catch {
let error = {
kind: "OverlargePointersNotImplementedError" as const,
pointerAsBN: rawValueAsBN
};
if (strict) {
throw new StopDecodingError(error);
}
return <Format.Errors.ErrorResult>{
//again with the TS failures...
type: dataType,
kind: "error" as const,
error
};
}
let startPosition = rawValueAsNumber + base;
debug("startPosition %d", startPosition);
let dynamic: boolean;
let size: number;
try {
({ dynamic, size } = abiSizeInfo(dataType, allocations));
} catch (error) {
return handleDecodingError(dataType, error, strict);
}
if (!dynamic) {
//this will only come up when called from stack.ts
let staticPointer = {
location,
start: startPosition,
length: size
};
return yield* decodeAbiReferenceStatic(
dataType,
staticPointer,
info,
options
);
}
let length: number;
let lengthAsBN: BN;
let rawLength: Uint8Array;
switch (dataType.typeClass) {
case "bytes":
case "string":
//initial word contains length (unless an override was given)
if (lengthOverride !== undefined) {
lengthAsBN = lengthOverride;
//note in this case we do *not* increment start position;
//if a length override is given, that means the given start
//position skips over the length word!
} else {
try {
rawLength = yield* read(
{
location,
start: startPosition,
length: Evm.Utils.WORD_SIZE
},
state
);
} catch (error) {
return handleDecodingError(dataType, error, strict);
}
lengthAsBN = Conversion.toBN(rawLength);
startPosition += Evm.Utils.WORD_SIZE; //increment start position after reading length
//so it'll be set up to read the data
}
if (strict && lengthAsBN.gtn(state[location].length)) {
//you may notice that the comparison is a bit crude; that's OK, this is
//just to prevent huge numbers from DOSing us, other errors will still
//be caught regardless
throw new StopDecodingError({
kind: "OverlongArrayOrStringStrictModeError" as const,
lengthAsBN,
dataLength: state[location].length
});
}
try {
length = lengthAsBN.toNumber();
} catch {
//note: if we're in this situation, we can assume we're not in strict mode,
//as the strict case was handled above
return <
| Format.Errors.BytesDynamicErrorResult
| Format.Errors.StringErrorResult
>{
//again with the TS failures...
type: dataType,
kind: "error" as const,
error: {
kind: "OverlongArraysAndStringsNotImplementedError" as const,
lengthAsBN
}
};
}
let childPointer: Pointer.AbiDataPointer = {
location,
start: startPosition,
length
};
return yield* Bytes.Decode.decodeBytes(
dataType,
childPointer,
info,
options
);
case "array":
if (dataType.kind === "static") {
//static-length array
lengthAsBN = dataType.length;
//note we don't increment start position; static arrays don't
//include a length word!
} else if (lengthOverride !== undefined) {
debug("override: %o", lengthOverride);
//dynamic-length array, but with length override
lengthAsBN = lengthOverride;
//we don't increment start position; if a length override was
//given, that means the pointer skipped the length word!
} else {
//dynamic-length array, read length from data
//initial word contains array length
try {
rawLength = yield* read(
{
location,
start: startPosition,
length: Evm.Utils.WORD_SIZE
},
state
);
} catch (error) {
return handleDecodingError(dataType, error, strict);
}
lengthAsBN = Conversion.toBN(rawLength);
startPosition += Evm.Utils.WORD_SIZE; //increment startPosition
//to next word, as first word was used for length
}
if (strict && lengthAsBN.gtn(state[location].length)) {
//you may notice that the comparison is a bit crude; that's OK, this is
//just to prevent huge numbers from DOSing us, other errors will still
//be caught regardless
throw new StopDecodingError({
kind: "OverlongArraysAndStringsNotImplementedError" as const,
lengthAsBN,
dataLength: state[location].length
});
}
try {
length = lengthAsBN.toNumber();
} catch {
//again, if we get here, we can assume we're not in strict mode
return {
type: dataType,
kind: "error" as const,
error: {
kind: "OverlongArraysAndStringsNotImplementedError" as const,
lengthAsBN
}
};
}
//note: I've written this fairly generically, but it is worth noting that
//since this array is of dynamic type, we know that if it's static length
//then size must be EVM.WORD_SIZE
let baseSize: number;
try {
baseSize = abiSizeInfo(dataType.baseType, allocations).size;
} catch (error) {
return handleDecodingError(dataType, error, strict);
}
let decodedChildren: Format.Values.Result[] = [];
for (let index = 0; index < length; index++) {
decodedChildren.push(
yield* decodeAbi(
dataType.baseType,
{
location,
start: startPosition + index * baseSize,
length: baseSize
},
info,
{ ...options, abiPointerBase: startPosition }
)
); //pointer base is always start of list, never the length
}
return {
type: dataType,
kind: "value" as const,
value: decodedChildren,
interpretations: {}
};
case "struct":
return yield* decodeAbiStructByPosition(
dataType,
location,
startPosition,
info,
options
);
case "tuple":
return yield* decodeAbiTupleByPosition(
dataType,
location,
startPosition,
info,
options
);
}
}
export function* decodeAbiReferenceStatic(
dataType: Format.Types.ReferenceType | Format.Types.TupleType,
pointer: Pointer.AbiDataPointer,
info: Evm.EvmInfo,
options: DecoderOptions = {}
): Generator<DecoderRequest, Format.Values.Result, Uint8Array | null> {
debug("static");
debug("pointer %o", pointer);
const location = pointer.location;
switch (dataType.typeClass) {
case "array":
//we're in the static case, so we know the array must be statically sized
const lengthAsBN = (<Format.Types.ArrayTypeStatic>dataType).length;
let length: number;
try {
length = lengthAsBN.toNumber();
} catch {
//note: since this is the static case, we don't bother including the stronger
//strict-mode guard against getting DOSed by large array sizes, since in this
//case we're not reading the size from the input; if there's a huge static size
//array, well, we'll just have to deal with it
let error = {
kind: "OverlongArraysAndStringsNotImplementedError" as const,
lengthAsBN
};
if (options.strictAbiMode) {
throw new StopDecodingError(error);
}
return {
type: dataType,
kind: "error" as const,
error
};
}
let baseSize: number;
try {
baseSize = abiSizeInfo(dataType.baseType, info.allocations.abi).size;
} catch (error) {
return handleDecodingError(dataType, error, options.strictAbiMode);
}
let decodedChildren: Format.Values.Result[] = [];
for (let index = 0; index < length; index++) {
decodedChildren.push(
yield* decodeAbi(
dataType.baseType,
{
location,
start: pointer.start + index * baseSize,
length: baseSize
},
info,
options
)
);
}
return {
type: dataType,
kind: "value" as const,
value: decodedChildren,
interpretations: {}
};
case "struct":
return yield* decodeAbiStructByPosition(
dataType,
location,
pointer.start,
info,
options
);
case "tuple":
return yield* decodeAbiTupleByPosition(
dataType,
location,
pointer.start,
info,
options
);
}
}
//note that this function takes the start position as a *number*; it does not take a pointer
function* decodeAbiStructByPosition(
dataType: Format.Types.StructType,
location: AbiLocation,
startPosition: number,
info: Evm.EvmInfo,
options: DecoderOptions = {}
): Generator<DecoderRequest, Format.Values.StructResult, Uint8Array | null> {
const {
allocations: { abi: allocations }
} = info;
const typeLocation = location === "calldata" ? "calldata" : null; //other abi locations are not valid type locations
const typeId = dataType.id;
const structAllocation = allocations[typeId];
if (!structAllocation) {
let error = {
kind: "UserDefinedTypeNotFoundError" as const,
type: dataType
};
if (options.strictAbiMode || options.allowRetry) {
throw new StopDecodingError(error, true);
//note that we allow a retry if we couldn't locate the allocation!
}
return {
type: dataType,
kind: "error" as const,
error
};
}
let decodedMembers: Format.Values.NameValuePair[] = [];
for (let index = 0; index < structAllocation.members.length; index++) {
const memberAllocation = structAllocation.members[index];
const memberPointer = memberAllocation.pointer;
const childPointer: Pointer.AbiDataPointer = {
location,
start: startPosition + memberPointer.start,
length: memberPointer.length
};
let memberName = memberAllocation.name;
let memberType = Format.Types.specifyLocation(
memberAllocation.type,
typeLocation
);
decodedMembers.push({
name: memberName,
value: yield* decodeAbi(memberType, childPointer, info, {
...options,
abiPointerBase: startPosition
})
//note that the base option is only needed in the dynamic case, but we're being indiscriminate
});
}
return {
type: dataType,
kind: "value" as const,
value: decodedMembers,
interpretations: {}
};
}
//note that this function takes the start position as a *number*; it does not take a pointer
function* decodeAbiTupleByPosition(
dataType: Format.Types.TupleType,
location: AbiLocation,
startPosition: number,
info: Evm.EvmInfo,
options: DecoderOptions = {}
): Generator<DecoderRequest, Format.Values.TupleResult, Uint8Array | null> {
//WARNING: This case is written in a way that involves a bunch of unnecessary recomputation!
//I'm writing it this way anyway for simplicity, to avoid rewriting the decoder
//However it may be worth revisiting this in the future if performance turns out to be a problem
//(changing this may be pretty hard though)
let decodedMembers: Format.Values.NameValuePair[] = [];
let position = startPosition;
for (const { name, type: memberType } of dataType.memberTypes) {
const memberSize = abiSizeInfo(memberType, info.allocations.abi).size;
const childPointer: Pointer.AbiDataPointer = {
location,
start: position,
length: memberSize
};
decodedMembers.push({
name,
value: yield* decodeAbi(memberType, childPointer, info, {
...options,
abiPointerBase: startPosition
})
//note that the base option is only needed in the dynamic case, but we're being indiscriminate
});
position += memberSize;
}
return {
type: dataType,
kind: "value" as const,
value: decodedMembers,
interpretations: {}
};
}