Skip to content

Commit

Permalink
🪚 OmniGraph™ Add EVM error parser (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Dec 6, 2023
1 parent 8c641a8 commit 9a3e724
Show file tree
Hide file tree
Showing 15 changed files with 481 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/utils-evm-test/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
3 changes: 3 additions & 0 deletions packages/utils-evm-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
artifacts
cache
deployments
2 changes: 2 additions & 0 deletions packages/utils-evm-test/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
11 changes: 11 additions & 0 deletions packages/utils-evm-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<p align="center">
<a href="https://layerzero.network">
<img alt="LayerZero" style="max-width: 500px" src="https://d3a2dpnnrypp5h.cloudfront.net/bridge-app/lz.png"/>
</a>
</p>

<h1 align="center">@layerzerolabs/utils-evm-test</h1>

## Development

This package provides integration tests for `@layerzerolabs/utils-evm` using `hardhat`.
37 changes: 37 additions & 0 deletions packages/utils-evm-test/contracts/Thrower.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 10 additions & 0 deletions packages/utils-evm-test/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import '@nomiclabs/hardhat-ethers'
import { HardhatUserConfig } from 'hardhat/types'

const config: HardhatUserConfig = {
solidity: {
version: '0.8.19',
},
}

export default config
35 changes: 35 additions & 0 deletions packages/utils-evm-test/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
196 changes: 196 additions & 0 deletions packages/utils-evm-test/test/errors/parser.test.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>`
*
* @returns `Promise<unknown>`
*/
const assertFailed = async (promise: Promise<unknown>): Promise<unknown> =>
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
})
})
9 changes: 9 additions & 0 deletions packages/utils-evm-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"exclude": ["dist", "node_modules"],
"include": ["src", "test", "*.config.ts"],
"compilerOptions": {
"module": "commonjs",
"types": ["node", "mocha"]
}
}
4 changes: 2 additions & 2 deletions packages/utils-evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
"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",
"@ethersproject/contracts": "^5.7.0",
"@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",
Expand All @@ -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"
}
Expand Down
39 changes: 39 additions & 0 deletions packages/utils-evm/src/errors/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export abstract class ContractError<TReason = unknown> extends Error {
public abstract readonly reason: TReason
}

export class UnknownError extends ContractError<undefined> {
public readonly reason = undefined

constructor(message = 'Unknown contract error') {
super(message)
}
}

export class PanicError extends ContractError<bigint> {
constructor(
public readonly reason: bigint,
message: string = `Contract panicked with code ${reason}`
) {
super(message)
}
}

export class RevertError extends ContractError<string> {
constructor(
public readonly reason: string,
message: string = `Contract reverted with reason '${reason}'`
) {
super(message)
}
}

export class CustomError extends ContractError<string> {
constructor(
public readonly reason: string,
public readonly args: unknown[],
message: string = `Contract reverted with custom error '${reason}'`
) {
super(message)
}
}
2 changes: 2 additions & 0 deletions packages/utils-evm/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './errors'
export * from './parser'
Loading

0 comments on commit 9a3e724

Please sign in to comment.