-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6957c63
commit ce3045d
Showing
17 changed files
with
491 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { z } from 'zod' | ||
|
||
export interface JsonRpcError extends Error { | ||
data: { | ||
code: number | ||
message?: string | ||
data?: string | ||
originalError?: JsonRpcError['data'] | ||
} | ||
} | ||
|
||
const jsonRpcErrorSchema = z.object({ | ||
data: z.object({ | ||
code: z.number(), | ||
message: z.string().optional(), | ||
data: z.string().optional(), | ||
}), | ||
}) | ||
|
||
export const isJsonRpcError = (error: unknown): error is JsonRpcError => { | ||
try { | ||
jsonRpcErrorSchema.parse(error) | ||
|
||
return true | ||
} catch { | ||
return false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { ContractFactories, KnownContracts } from '@gnosis.pm/zodiac' | ||
import { AbiCoder } from 'ethers' | ||
import type { JsonRpcError } from './JsonRpcError' | ||
|
||
const RolesV1Interface = | ||
ContractFactories[KnownContracts.ROLES_V1].createInterface() | ||
const RolesV1PermissionsInterface = | ||
ContractFactories[KnownContracts.PERMISSIONS].createInterface() | ||
const RolesV2Interface = | ||
ContractFactories[KnownContracts.ROLES_V2].createInterface() | ||
|
||
export function getRevertData(error: JsonRpcError) { | ||
// The errors thrown when a transaction is reverted use different formats, depending on: | ||
// - wallet (MetaMask vs. WalletConnect) | ||
// - RPC provider (Infura vs. Alchemy vs. Tenderly) | ||
// - client library (ethers vs. directly using the EIP-1193 provider) | ||
|
||
// first, drill through potential error wrappings down to the original error | ||
while (typeof error === 'object' && (error as any).error) { | ||
error = (error as any).error | ||
} | ||
|
||
// Here we try to extract the revert reason in any of the possible formats | ||
const message = | ||
typeof error.data === 'string' | ||
? error.data | ||
: error.data?.originalError?.data || | ||
error.data?.data || | ||
error.data?.originalError?.message || | ||
error.data?.message || | ||
error.message | ||
|
||
const prefix = 'Reverted 0x' | ||
return message.startsWith(prefix) | ||
? message.substring(prefix.length - 2) | ||
: message | ||
} | ||
|
||
export function decodeGenericError(error: JsonRpcError) { | ||
const revertData = getRevertData(error) | ||
|
||
// Solidity `revert "reason string"` will revert with the data encoded as selector of `Error(string)` followed by the ABI encoded string param | ||
if (revertData.startsWith('0x08c379a0')) { | ||
try { | ||
const [reason] = AbiCoder.defaultAbiCoder().decode( | ||
['string'], | ||
'0x' + revertData.slice(10), // skip over selector | ||
) | ||
return reason as string | ||
} catch { | ||
return revertData | ||
} | ||
} | ||
|
||
return revertData | ||
} | ||
|
||
export function decodeRolesV1Error(error: JsonRpcError) { | ||
const revertData = getRevertData(error) | ||
if (revertData.startsWith('0x')) { | ||
try { | ||
return ( | ||
RolesV1Interface.parseError(revertData) || | ||
RolesV1PermissionsInterface.parseError(revertData) | ||
) | ||
} catch { | ||
// ignore | ||
} | ||
} | ||
return null | ||
} | ||
|
||
export function decodeRolesV2Error(error: JsonRpcError) { | ||
const revertData = getRevertData(error) | ||
|
||
if (revertData.startsWith('0x')) { | ||
try { | ||
return RolesV2Interface.parseError(revertData) | ||
} catch { | ||
// ignore | ||
} | ||
} | ||
|
||
return null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { ZERO_ADDRESS } from '@zodiac/chains' | ||
import { getRolesWaypoint } from '@zodiac/modules' | ||
import type { ExecutionRoute } from '@zodiac/schema' | ||
import type { JsonRpcProvider } from 'ethers' | ||
import { AccountType, parsePrefixedAddress } from 'ser-kit' | ||
import { decodeGenericError } from './decodeError' | ||
import { handleRolesV1Error } from './handleRolesV1Error' | ||
import { handleRolesV2Error } from './handleRolesV2Error' | ||
import { isSmartContractAddress } from './isSmartContractAddress' | ||
import { isJsonRpcError } from './JsonRpcError' | ||
import { maybeGetRoleId } from './maybeGetRoleId' | ||
import { wrapRequest } from './wrapRequest' | ||
|
||
type Error = { error: true; message: string } | ||
type Success = { error: false } | ||
|
||
type Result = Error | Success | ||
|
||
export async function dryRun( | ||
provider: JsonRpcProvider, | ||
route: ExecutionRoute, | ||
): Promise<Result> { | ||
if ( | ||
route.initiator == null || | ||
parsePrefixedAddress(route.initiator) === ZERO_ADDRESS | ||
) { | ||
return { error: true, message: 'This route is not connected to a wallet.' } | ||
} | ||
|
||
if (parsePrefixedAddress(route.avatar) === ZERO_ADDRESS) { | ||
return { error: true, message: 'This route has no target.' } | ||
} | ||
|
||
const rolesWaypoint = getRolesWaypoint(route) | ||
|
||
if ( | ||
rolesWaypoint != null && | ||
!(await isSmartContractAddress(rolesWaypoint.account.address, provider)) | ||
) { | ||
return { error: true, message: 'Module address is not a smart contract.' } | ||
} | ||
|
||
if ( | ||
!(await isSmartContractAddress( | ||
parsePrefixedAddress(route.avatar), | ||
provider, | ||
)) | ||
) { | ||
return { error: true, message: 'Avatar is not a smart contract.' } | ||
} | ||
|
||
const request = wrapRequest({ | ||
request: { | ||
to: ZERO_ADDRESS, | ||
data: '0x00000000', | ||
from: parsePrefixedAddress(route.avatar), | ||
}, | ||
route, | ||
revertOnError: false, | ||
}) | ||
|
||
// TODO enable this once we can query role members from ser | ||
// if (!request.from && connection.roleId) { | ||
// // If pilotAddress is not yet determined, we will use a random member of the specified role | ||
// request.from = await getRoleMember(connection) | ||
// } | ||
|
||
try { | ||
await provider.estimateGas(request) | ||
|
||
return { error: false } | ||
} catch (error) { | ||
if (!isJsonRpcError(error)) { | ||
return { error: true, message: 'Unknown dry run error.' } | ||
} | ||
|
||
if ( | ||
rolesWaypoint == null || | ||
rolesWaypoint.account.type !== AccountType.ROLES | ||
) { | ||
return { error: true, message: decodeGenericError(error) } | ||
} | ||
// For the Roles mod, we actually expect the dry run to fail with TargetAddressNotAllowed() | ||
// In case we see any other error, we try to help the user identify the problem. | ||
|
||
const { account } = rolesWaypoint | ||
|
||
switch (account.version) { | ||
case 1: { | ||
const message = handleRolesV1Error( | ||
error, | ||
maybeGetRoleId(rolesWaypoint) ?? '0', | ||
) | ||
|
||
if (message == null) { | ||
return { error: false } | ||
} | ||
|
||
return { | ||
error: true, | ||
message, | ||
} | ||
} | ||
case 2: { | ||
const message = handleRolesV2Error( | ||
error, | ||
maybeGetRoleId(rolesWaypoint) ?? '0', | ||
) | ||
|
||
if (message == null) { | ||
return { error: false } | ||
} | ||
|
||
return { | ||
error: true, | ||
message, | ||
} | ||
} | ||
default: { | ||
console.warn('Unexpected dry run error', error) | ||
|
||
return { error: true, message: 'Unexpected dry run error' } | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.