Skip to content

Commit

Permalink
EVM: Improve error messages in function and event decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
vanruch committed Jun 20, 2024
1 parent 3c0a6eb commit 0aef375
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 39 deletions.
29 changes: 19 additions & 10 deletions evm/evm-abi/src/abi-components/event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Simplify } from '../indexed'
import { bytes32, Src, type Codec, type DecodedStruct, type Struct } from '@subsquid/evm-codec'
import {EventEmptyTopicsError, EventInvalidSignatureError, EventTopicCountMismatchError} from '../errors'
import {
EventDecodingError,
EventEmptyTopicsError,
EventInvalidSignatureError,
EventTopicCountMismatchError
} from '../errors'

export interface EventRecord {
topics: string[]
Expand All @@ -23,7 +28,7 @@ export class AbiEvent<const T extends EventArgs> {
public readonly params: IndexedCodecs<T>
private readonly topicCount: number

constructor(public readonly topic: string, args: T) {
constructor(public readonly topic: string, public readonly signature: string, args: T) {
this.topicCount = 1
this.params = {} as IndexedCodecs<T>
for (const i in args) {
Expand Down Expand Up @@ -56,17 +61,21 @@ export class AbiEvent<const T extends EventArgs> {
if (!this.checkSignature(rec)) {
throw new EventInvalidSignatureError({targetSig: this.topic, sig: rec.topics[0]})
}

const src = new Src(Buffer.from(rec.data.slice(2), 'hex'))
const result = {} as any
let topicCounter = 1
for (let i in this.params) {
if (this.params[i].indexed) {
const topic = rec.topics[topicCounter++]
const topicSrc = new Src(Buffer.from(topic.slice(2), 'hex'))
result[i] = this.params[i].decode(topicSrc)
} else {
result[i] = this.params[i].decode(src)
try {
if (this.params[i].indexed) {
const topic = rec.topics[topicCounter++]
const topicSrc = new Src(Buffer.from(topic.slice(2), 'hex'))
result[i] = this.params[i].decode(topicSrc)
} else {
result[i] = this.params[i].decode(src)
}
} catch (e: any) {
throw new EventDecodingError(this.signature, i, rec.data, e.message)
}
}
return result
Expand All @@ -81,4 +90,4 @@ export class AbiEvent<const T extends EventArgs> {
}
}

export const event = <const T extends Struct>(topic: string, args: T) => new AbiEvent<T>(topic, args)
export const event = <const T extends Struct>(topic: string, signature: string, args: T) => new AbiEvent<T>(topic, signature, args)
41 changes: 24 additions & 17 deletions evm/evm-abi/src/abi-components/function.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert'
import {type Codec, type Struct, type DecodedStruct, type EncodedStruct, Sink, Src} from '@subsquid/evm-codec'
import {FunctionInvalidSignatureError} from '../errors'
import {FunctionInvalidSignatureError, FunctionResultDecodeError, FunctionCalldataDecodeError} from '../errors'

export interface CallRecord {
input: string
Expand All @@ -20,13 +20,6 @@ export type FunctionReturn<T extends AbiFunction<any, any>> = T extends AbiFunct

export type FunctionArguments<T extends AbiFunction<any, any>> = T extends AbiFunction<infer U, any> ? EncodedStruct<U> : never

export class UnexpectedFunctionError extends Error {
constructor(expectedSignature: string, gotSignature: string) {
super(`unexpected function signature. Expected: ${expectedSignature}, got: ${gotSignature}`)
this.name = 'UnexpectedFunctionError';
}
}

export class AbiFunction<const T extends Struct, const R extends Codec<any> | Struct | undefined> {
readonly #selector: Buffer
private readonly slotsCount: number
Expand All @@ -35,7 +28,7 @@ export class AbiFunction<const T extends Struct, const R extends Codec<any> | St
return this.selector
}

constructor(public selector: string, public readonly args: T, public readonly returnType?: R, public isView = false) {
constructor(public selector: string, public signature: string, public readonly args: T, public readonly returnType?: R, public isView = false) {
assert(selector.startsWith('0x'), 'selector must start with 0x')
assert(selector.length === 10, 'selector must be 4 bytes long')
this.#selector = Buffer.from(selector.slice(2), 'hex')
Expand All @@ -44,7 +37,7 @@ export class AbiFunction<const T extends Struct, const R extends Codec<any> | St
}

is(calldata: string | CallRecord) {
return this.checkSighature(typeof calldata === 'string' ? calldata : calldata.input)
return this.checkSignature(typeof calldata === 'string' ? calldata : calldata.input)
}

encode(args: EncodedStruct<T>) {
Expand All @@ -58,13 +51,17 @@ export class AbiFunction<const T extends Struct, const R extends Codec<any> | St
decode(calldata: string | CallRecord): DecodedStruct<T> {
const input = typeof calldata === 'string' ? calldata : calldata.input

if (!this.checkSighature(input)) {
if (!this.checkSignature(input)) {
throw new FunctionInvalidSignatureError({targetSig: this.selector, sig: input.slice(0, this.selector.length)})
}
const src = new Src(Buffer.from(input.slice(10), 'hex'))
const result = {} as any
for (let i in this.args) {
result[i] = this.args[i].decode(src)
try {
result[i] = this.args[i].decode(src)
} catch (e: any) {
throw new FunctionCalldataDecodeError(this.signature, i, e.message, input)
}
}
return result
}
Expand All @@ -79,29 +76,39 @@ export class AbiFunction<const T extends Struct, const R extends Codec<any> | St
}
const src = new Src(Buffer.from(output.slice(2), 'hex'))
if (this.isCodecs(this.returnType)) {
return this.returnType.decode(src) as any
try {
return this.returnType.decode(src) as any
} catch (e: any) {
throw new FunctionResultDecodeError(this.signature, '', e.message, output)
}
}
const result = {} as any
for (let i in this.returnType) {
const codec = this.returnType[i] as Codec<any>
result[i] = codec.decode(src)
try {
result[i] = codec.decode(src)
} catch (e: any) {
throw new FunctionResultDecodeError(this.signature, i, e.message, output)
}
}
return result
}

private checkSighature(val: string) {
private checkSignature(val: string) {
return val.startsWith(this.selector)
}
}

export const fun = <const T extends Struct, const R extends Codec<any> | Struct | undefined>(
selector: string,
signature: string,
args: T,
returnType?: R,
) => new AbiFunction<T, R>(signature, args, returnType, false)
) => new AbiFunction<T, R>(selector, signature, args, returnType, false)

export const viewFun = <const T extends Struct, const R extends Codec<any> | Struct | undefined>(
selector: string,
signature: string,
args: T,
returnType?: R,
) => new AbiFunction<T, R>(signature, args, returnType, true)
) => new AbiFunction<T, R>(selector, signature, args, returnType, true)
28 changes: 28 additions & 0 deletions evm/evm-abi/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,31 @@ export class FunctionInvalidSignatureError extends DecodingError {
super(`Invalid function signature. Expected "${targetSig}", but received ${sig}.`)
}
}

export class EventDecodingError extends Error {
constructor(signature: string, argumentName: string, data: string, message: any) {
super(`Error decoding argument ${argumentName} of event ${signature}:\n\n${data}\n\n${message}`)
this.name = 'EventDecodingError'
}
}

export class UnexpectedFunctionError extends Error {
constructor(expectedSignature: string, gotSignature: string) {
super(`unexpected function signature. Expected: ${expectedSignature}, got: ${gotSignature}`)
this.name = 'UnexpectedFunctionError';
}
}

export class FunctionCalldataDecodeError extends Error {
constructor(functionSignature: string, argumentName: string, message: string, data: string) {
super(`Error decoding argument ${argumentName} of function ${functionSignature}:\n\n${data}\n\n${message}`)
this.name = 'FunctionCalldataDecodeError';
}
}

export class FunctionResultDecodeError extends Error {
constructor(functionSignature: string, argumentName: string, message: string, data: string) {
super(`Error decoding return argument ${argumentName} of function ${functionSignature}:\n\n${data}\n\n${message}`)
this.name = 'FunctionResultDecodeError';
}
}
17 changes: 12 additions & 5 deletions evm/evm-abi/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
export { ContractBase } from './contract-base'
export {ContractBase} from './contract-base'

export { indexed } from './indexed'
export { fun, viewFun, AbiFunction, type FunctionReturn, type FunctionArguments } from './abi-components/function'
export { event, AbiEvent, type EventRecord, type EventParams } from './abi-components/event'
export {indexed} from './indexed'
export {
fun,
viewFun,
AbiFunction,
type FunctionReturn,
type FunctionArguments,
} from './abi-components/function'
export {event, AbiEvent, type EventRecord, type EventParams} from './abi-components/event'
export * from './errors'
import keccak256 from 'keccak256'
export { keccak256 }

export {keccak256}
14 changes: 7 additions & 7 deletions evm/evm-typegen/src/typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class Typegen {
this.out.block(`export const events =`, () => {
for (let e of events) {
this.out.line(
`${this.getPropName(e)}: event("${this.topic0(e)}", {${this.toTypes(
`${this.getPropName(e)}: event("${this.topic0(e)}", ${this.signature(e)}, {${this.toTypes(
e.inputs,
)}}),`,
)
Expand All @@ -64,7 +64,7 @@ export class Typegen {
}

private topic0(e: AbiEvent): string {
return `0x${keccak256(this.sighash(e)).toString('hex')}`
return `0x${keccak256(this.signature(e)).toString('hex')}`
}

private toTypes(inputs: readonly AbiParameter[]): string {
Expand All @@ -90,14 +90,14 @@ export class Typegen {
this.out.line(
`${this.getPropName(f)}: ${funType}("${this.functionSelector(
f,
)}", {${this.toTypes(f.inputs)}}, ${returnType}),`,
)}", ${this.signature(f)}, {${this.toTypes(f.inputs)}}, ${returnType}),`,
)
}
})
}

private functionSelector(f: AbiFunction): string {
const sighash = this.sighash(f)
const sighash = this.signature(f)
return `0x${keccak256(sighash).slice(0, 4).toString('hex')}`
}

Expand Down Expand Up @@ -139,7 +139,7 @@ export class Typegen {
)})${arrayBrackets}`
}

private sighash(item: AbiEvent | AbiFunction): string {
private signature(item: AbiEvent | AbiFunction): string {
return `${item.name}(${item.inputs
.map((param) => this.cannonicalType(param))
.join(',')})`
Expand All @@ -149,15 +149,15 @@ export class Typegen {
if (this.getOverloads(item) == 1) {
return item.name
} else {
return `"${this.sighash(item)}"`
return `"${this.signature(item)}"`
}
}

private getPropNameGetter(item: AbiEvent | AbiFunction): string {
if (this.getOverloads(item) == 1) {
return '.' + item.name
} else {
return `["${this.sighash(item)}"]`
return `["${this.signature(item)}"]`
}
}

Expand Down

0 comments on commit 0aef375

Please sign in to comment.