diff --git a/packages/utils-evm-test/.eslintignore b/packages/utils-evm-test/.eslintignore
new file mode 100644
index 000000000..db4c6d9b6
--- /dev/null
+++ b/packages/utils-evm-test/.eslintignore
@@ -0,0 +1,2 @@
+dist
+node_modules
\ No newline at end of file
diff --git a/packages/utils-evm-test/.gitignore b/packages/utils-evm-test/.gitignore
new file mode 100644
index 000000000..803d4166c
--- /dev/null
+++ b/packages/utils-evm-test/.gitignore
@@ -0,0 +1,3 @@
+artifacts
+cache
+deployments
\ No newline at end of file
diff --git a/packages/utils-evm-test/.prettierignore b/packages/utils-evm-test/.prettierignore
new file mode 100644
index 000000000..763301fc0
--- /dev/null
+++ b/packages/utils-evm-test/.prettierignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/
\ No newline at end of file
diff --git a/packages/utils-evm-test/README.md b/packages/utils-evm-test/README.md
new file mode 100644
index 000000000..cc0bb56e4
--- /dev/null
+++ b/packages/utils-evm-test/README.md
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+@layerzerolabs/utils-evm-test
+
+## Development
+
+This package provides integration tests for `@layerzerolabs/utils-evm` using `hardhat`.
diff --git a/packages/utils-evm-test/contracts/Thrower.sol b/packages/utils-evm-test/contracts/Thrower.sol
new file mode 100644
index 000000000..aa5420006
--- /dev/null
+++ b/packages/utils-evm-test/contracts/Thrower.sol
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.19;
+
+contract Thrower {
+ error CustomErrorWithNoArguments();
+ error CustomErrorWithAnArgument(string message);
+
+ function throwWithAssert() external pure {
+ assert(0 == 1);
+ }
+
+ // For some reason in hardhat node this function does not revert
+ function throwWithRevertAndNoArguments() external pure {
+ revert();
+ }
+
+ function throwWithRevertAndArgument(string calldata message) external pure {
+ revert(message);
+ }
+
+ // For some reason in hardhat node this function does not revert
+ function throwWithRequireAndNoArguments() external pure {
+ require(0 == 1);
+ }
+
+ function throwWithRequireAndArgument(string calldata message) external pure {
+ require(0 == 1, message);
+ }
+
+ function throwWithCustomErrorAndNoArguments() external pure {
+ revert CustomErrorWithNoArguments();
+ }
+
+ function throwWithCustomErrorAndArgument(string calldata message) external pure {
+ revert CustomErrorWithAnArgument(message);
+ }
+}
diff --git a/packages/utils-evm-test/hardhat.config.ts b/packages/utils-evm-test/hardhat.config.ts
new file mode 100644
index 000000000..14f906fcc
--- /dev/null
+++ b/packages/utils-evm-test/hardhat.config.ts
@@ -0,0 +1,10 @@
+import '@nomiclabs/hardhat-ethers'
+import { HardhatUserConfig } from 'hardhat/types'
+
+const config: HardhatUserConfig = {
+ solidity: {
+ version: '0.8.19',
+ },
+}
+
+export default config
diff --git a/packages/utils-evm-test/package.json b/packages/utils-evm-test/package.json
new file mode 100644
index 000000000..e4ed335f6
--- /dev/null
+++ b/packages/utils-evm-test/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@layerzerolabs/utils-evm-test",
+ "version": "0.0.1",
+ "private": true,
+ "description": "Integration tests for ua-utils-evm-hardhat for V2",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/LayerZero-Labs/lz-utils.git",
+ "directory": "packages/utils-evm-test"
+ },
+ "license": "MIT",
+ "scripts": {
+ "lint": "npx eslint '**/*.{js,ts,json}'",
+ "test": "npx hardhat test"
+ },
+ "devDependencies": {
+ "@ethersproject/abi": "^5.7.0",
+ "@ethersproject/abstract-provider": "^5.7.0",
+ "@ethersproject/abstract-signer": "^5.7.0",
+ "@ethersproject/contracts": "^5.7.0",
+ "@ethersproject/providers": "^5.7.0",
+ "@layerzerolabs/lz-definitions": "~1.5.68",
+ "@layerzerolabs/test-utils": "~0.0.1",
+ "@layerzerolabs/utils": "~0.0.1",
+ "@layerzerolabs/utils-evm": "~0.0.1",
+ "@nomiclabs/hardhat-ethers": "^2.2.3",
+ "@types/mocha": "^10.0.6",
+ "chai": "^4.3.10",
+ "ethers": "^5.7.0",
+ "fast-check": "^3.14.0",
+ "hardhat": "^2.19.0",
+ "ts-node": "^10.9.1",
+ "typescript": "^5.2.2"
+ }
+}
\ No newline at end of file
diff --git a/packages/utils-evm-test/test/errors/parser.test.ts b/packages/utils-evm-test/test/errors/parser.test.ts
new file mode 100644
index 000000000..33a2fbec8
--- /dev/null
+++ b/packages/utils-evm-test/test/errors/parser.test.ts
@@ -0,0 +1,196 @@
+import fc from 'fast-check'
+import hre from 'hardhat'
+import { expect } from 'chai'
+import { Contract } from '@ethersproject/contracts'
+import {
+ createErrorParser,
+ PanicError,
+ RevertError,
+ CustomError,
+ UnknownError,
+ OmniContractFactory,
+} from '@layerzerolabs/utils-evm'
+import { OmniError } from '@layerzerolabs/utils'
+import { pointArbitrary } from '@layerzerolabs/test-utils'
+
+describe('errors/parser', () => {
+ describe('createErrorParser', () => {
+ const CONTRACT_NAME = 'Thrower'
+
+ let contract: Contract
+ let omniContractFactory: OmniContractFactory
+
+ /**
+ * Helper utility that swaps the promise resolution for rejection and other way around
+ *
+ * This is useful for the below tests since we are testing that promises reject
+ * and want to get their rejection values.
+ *
+ * @param promise `Promise`
+ *
+ * @returns `Promise`
+ */
+ const assertFailed = async (promise: Promise): Promise =>
+ promise.then(
+ (result) => {
+ expect.fail(`Expected a promise to always reject but it resolved with ${JSON.stringify(result)}`)
+ },
+ (error) => error
+ )
+
+ before(async () => {
+ const contractFactory = await hre.ethers.getContractFactory(CONTRACT_NAME)
+
+ contract = await contractFactory.deploy()
+ omniContractFactory = async ({ eid, address }) => ({ eid, contract: contractFactory.attach(address) })
+ })
+
+ it('should pass an error through if it already is a ContractError', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const omniError: OmniError = { error: new RevertError('A reason is worth a million bytes'), point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(RevertError)
+ expect(parsedError.error.reason).to.equal('A reason is worth a million bytes')
+ })
+ )
+ })
+
+ it('should parse assert/panic', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const error = await assertFailed(contract.throwWithAssert())
+ const omniError: OmniError = { error, point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(PanicError)
+ expect(parsedError.error.reason).to.eql(BigInt(1))
+ })
+ )
+ })
+
+ it('should parse revert with arguments', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const error = await assertFailed(contract.throwWithRevertAndArgument('my bad'))
+ const omniError: OmniError = { error, point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(RevertError)
+ expect(parsedError.error.reason).to.eql('my bad')
+ })
+ )
+ })
+
+ it('should parse require with an argument', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const error = await assertFailed(contract.throwWithRequireAndArgument('my bad'))
+ const omniError: OmniError = { error, point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(RevertError)
+ expect(parsedError.error.reason).to.eql('my bad')
+ })
+ )
+ })
+
+ it('should parse require with a custom error with no arguments', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const error = await assertFailed(contract.throwWithCustomErrorAndNoArguments())
+ const omniError: OmniError = { error, point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(CustomError)
+ expect(parsedError.error.reason).to.eql('CustomErrorWithNoArguments')
+ expect((parsedError.error as CustomError).args).to.eql([])
+ })
+ )
+ })
+
+ it('should parse require with a custom error with an argument', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const error = await assertFailed(contract.throwWithCustomErrorAndArgument('my bad'))
+ const omniError: OmniError = { error, point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(CustomError)
+ expect(parsedError.error.reason).to.eql('CustomErrorWithAnArgument')
+ expect((parsedError.error as CustomError).args).to.eql(['my bad'])
+ })
+ )
+ })
+
+ it('should parse string', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const omniError: OmniError = { error: 'some weird error', point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(UnknownError)
+ expect(parsedError.error.reason).to.be.undefined
+ expect(parsedError.error.message).to.eql('Unknown error: some weird error')
+ })
+ )
+ })
+
+ it('should parse an Error', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, async (point) => {
+ const omniError: OmniError = { error: new Error('some weird error'), point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(UnknownError)
+ expect(parsedError.error.reason).to.be.undefined
+ expect(parsedError.error.message).to.eql('Unknown error: Error: some weird error')
+ })
+ )
+ })
+
+ it('should never reject', async () => {
+ const errorParser = createErrorParser(omniContractFactory)
+
+ await fc.assert(
+ fc.asyncProperty(pointArbitrary, fc.anything(), async (point, error) => {
+ const omniError: OmniError = { error, point }
+ const parsedError = await errorParser(omniError)
+
+ expect(parsedError.point).to.eql(point)
+ expect(parsedError.error).to.be.instanceOf(UnknownError)
+ expect(parsedError.error.reason).to.be.undefined
+ expect(parsedError.error.message).to.eql(`Unknown error: ${error}`)
+ })
+ )
+ })
+
+ // FIXME Write tests for throwWithRevertAndNoArguments - in hardhat node they don't seem to revert
+ // FIXME Write tests for throwWithRequireAndNoArguments - in hardhat node they don't seem to revert
+ })
+})
diff --git a/packages/utils-evm-test/tsconfig.json b/packages/utils-evm-test/tsconfig.json
new file mode 100644
index 000000000..1e471dfdd
--- /dev/null
+++ b/packages/utils-evm-test/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "exclude": ["dist", "node_modules"],
+ "include": ["src", "test", "*.config.ts"],
+ "compilerOptions": {
+ "module": "commonjs",
+ "types": ["node", "mocha"]
+ }
+}
diff --git a/packages/utils-evm/package.json b/packages/utils-evm/package.json
index a3dc66e25..8d8fa3edc 100644
--- a/packages/utils-evm/package.json
+++ b/packages/utils-evm/package.json
@@ -39,6 +39,7 @@
"p-memoize": "~4.0.1"
},
"devDependencies": {
+ "@ethersproject/abi": "^5.7.0",
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/abstract-signer": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
@@ -46,7 +47,6 @@
"@ethersproject/providers": "^5.7.0",
"@layerzerolabs/lz-definitions": "~1.5.68",
"@layerzerolabs/test-utils": "~0.0.1",
- "@layerzerolabs/ua-utils": "~0.1.0",
"@layerzerolabs/utils": "~0.0.1",
"@types/jest": "^29.5.10",
"fast-check": "^3.14.0",
@@ -58,12 +58,12 @@
"zod": "^3.22.4"
},
"peerDependencies": {
+ "@ethersproject/abi": "^5.7.0",
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/abstract-signer": "^5.7.0",
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@layerzerolabs/lz-definitions": "~1.5.68",
- "@layerzerolabs/ua-utils": "~0.1.0",
"@layerzerolabs/utils": "~0.0.1",
"zod": "^3.22.4"
}
diff --git a/packages/utils-evm/src/errors/errors.ts b/packages/utils-evm/src/errors/errors.ts
new file mode 100644
index 000000000..2efb6a479
--- /dev/null
+++ b/packages/utils-evm/src/errors/errors.ts
@@ -0,0 +1,39 @@
+export abstract class ContractError extends Error {
+ public abstract readonly reason: TReason
+}
+
+export class UnknownError extends ContractError {
+ public readonly reason = undefined
+
+ constructor(message = 'Unknown contract error') {
+ super(message)
+ }
+}
+
+export class PanicError extends ContractError {
+ constructor(
+ public readonly reason: bigint,
+ message: string = `Contract panicked with code ${reason}`
+ ) {
+ super(message)
+ }
+}
+
+export class RevertError extends ContractError {
+ constructor(
+ public readonly reason: string,
+ message: string = `Contract reverted with reason '${reason}'`
+ ) {
+ super(message)
+ }
+}
+
+export class CustomError extends ContractError {
+ constructor(
+ public readonly reason: string,
+ public readonly args: unknown[],
+ message: string = `Contract reverted with custom error '${reason}'`
+ ) {
+ super(message)
+ }
+}
diff --git a/packages/utils-evm/src/errors/index.ts b/packages/utils-evm/src/errors/index.ts
new file mode 100644
index 000000000..f73769133
--- /dev/null
+++ b/packages/utils-evm/src/errors/index.ts
@@ -0,0 +1,2 @@
+export * from './errors'
+export * from './parser'
diff --git a/packages/utils-evm/src/errors/parser.ts b/packages/utils-evm/src/errors/parser.ts
new file mode 100644
index 000000000..7be417ee7
--- /dev/null
+++ b/packages/utils-evm/src/errors/parser.ts
@@ -0,0 +1,124 @@
+import { defaultAbiCoder } from '@ethersproject/abi'
+import type { OmniContract, OmniContractFactory } from '@/omnigraph/types'
+import type { OmniError } from '@layerzerolabs/utils'
+import { ContractError, CustomError, UnknownError, PanicError, RevertError } from './errors'
+import { BigNumberishBigintSchema } from '../schema'
+
+/**
+ * Creates an asynchronous error parser for EVM contract errors.
+ *
+ * This parser is capable of turning `unknown` `OmniError` instances to typed ones
+ *
+ * @param contractFactory `OmniContractFactory`
+ *
+ * @returns `(omniError: OmniError): Promise>` `OmniError` parser
+ */
+export const createErrorParser =
+ (contractFactory: OmniContractFactory) =>
+ async ({ error, point }: OmniError): Promise> => {
+ try {
+ // If the error already is a ContractError, we'll continue
+ if (error instanceof ContractError) return { error, point }
+
+ // If the error is unknown we'll try to decode basic errors
+ const candidates = getErrorDataCandidates(error)
+ const [basicError] = candidates.flatMap(basicDecoder)
+ if (basicError != null) return { point, error: basicError }
+
+ // Then we'll try to decode custom errors
+ const contract = await contractFactory(point)
+ const contractDecoder = createContractDecoder(contract)
+ const [customError] = candidates.flatMap(contractDecoder)
+ if (customError != null) return { point, error: customError }
+
+ // If none of the decoding works, we'll send a generic error back
+ return { point, error: new UnknownError(`Unknown error: ${error}`) }
+ } catch {
+ // If we fail, we send an unknown error back
+ return {
+ point,
+ error: new UnknownError(`Unexpected error: ${error}`),
+ }
+ }
+ }
+
+// If a contract reverts using revert, the error data will be prefixed with this beauty
+const REVERT_ERROR_PREFIX = '0x08c379a0'
+
+// If a contract reverts with assert, the error data will be prefixed with this beauty
+const PANIC_ERROR_PREFIX = '0x4e487b71'
+
+/**
+ * Basic decoder can decode a set of common errors without having access to contract ABIs
+ *
+ * @param data `string` Error revert data
+ *
+ * @returns `ContractError[]` Decoded errors, if any
+ */
+const basicDecoder = (data: string): ContractError[] => {
+ if (data === '' || data === '0x') return [new UnknownError(`Reverted with empty data`)]
+
+ // This covers the case for assert()
+ if (data.startsWith(PANIC_ERROR_PREFIX)) {
+ const reason = data.slice(PANIC_ERROR_PREFIX.length)
+
+ try {
+ const [decodedRawReason] = defaultAbiCoder.decode(['uint256'], `0x${reason}`)
+ const decodedReason = BigNumberishBigintSchema.parse(decodedRawReason)
+
+ return [new PanicError(decodedReason)]
+ } catch {
+ return [new PanicError(BigInt(-1), `Reason unknown, ABI decoding failed. The raw reason was '0x${reason}'`)]
+ }
+ }
+
+ // This covers the case for revert() and reject()
+ if (data.startsWith(REVERT_ERROR_PREFIX)) {
+ const reason = data.slice(REVERT_ERROR_PREFIX.length)
+
+ try {
+ const [decodedReason] = defaultAbiCoder.decode(['string'], `0x${reason}`)
+
+ return [new RevertError(decodedReason)]
+ } catch {
+ return [new RevertError(`Reason unknown, ABI decoding failed. The raw reason was '0x${reason}'`)]
+ }
+ }
+
+ return []
+}
+
+/**
+ * Contract decoder uses the contract ABIs to decode error revert string
+ *
+ * @param contract `OmniContract`
+ *
+ * @returns `(data: string) => ContractError[]` Custom error decoder
+ */
+const createContractDecoder =
+ ({ contract }: OmniContract) =>
+ (data: string): ContractError[] => {
+ try {
+ const errorDescription = contract.interface.parseError(data)
+
+ return [new CustomError(errorDescription.name, [...errorDescription.args])]
+ } catch {
+ return []
+ }
+ }
+
+/**
+ * Helper function that traverses an unknown error and agthers all the fields
+ * that could possibly contain the revert data.
+ *
+ * The results are order from the most specific one to the least specific one
+ * since the function above will prioritize the results in front of the list
+ *
+ * @param error `unknown`
+ *
+ * @returns `string[]` A list of possible error revert strings
+ */
+const getErrorDataCandidates = (error: unknown): string[] =>
+ [(error as any)?.error?.data?.data, (error as any)?.error?.data, (error as any)?.data].filter(
+ (candidate: unknown) => typeof candidate === 'string'
+ )
diff --git a/packages/utils-evm/src/index.ts b/packages/utils-evm/src/index.ts
index e922e191b..f20ce5279 100644
--- a/packages/utils-evm/src/index.ts
+++ b/packages/utils-evm/src/index.ts
@@ -1,4 +1,5 @@
export * from './address'
+export * from './errors'
export * from './omnigraph'
export * from './provider'
export * from './schema'
diff --git a/packages/utils/src/omnigraph/types.ts b/packages/utils/src/omnigraph/types.ts
index 860202f8d..089a96c65 100644
--- a/packages/utils/src/omnigraph/types.ts
+++ b/packages/utils/src/omnigraph/types.ts
@@ -21,6 +21,14 @@ export interface OmniVector {
to: OmniPoint
}
+/**
+ * OmniError represents an arbitrary error that occurred on a particular point in omniverse.
+ */
+export interface OmniError {
+ point: OmniPoint
+ error: TError
+}
+
/**
* OmniNode represents a point in omniverse
* with an additional piece of information attached