Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Display readable Safe contract errors #2911

Merged
merged 14 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/InfiniteScroll/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,16 @@ export const InfiniteScroll = ({ children, hasMore, next, config }: InfiniteScro
})

useEffect(() => {
if (inView && hasMore) {
// Avoid memory leak - queue/history have separate InfiniteScroll wrappers
let isMounted = true

if (isMounted && inView && hasMore) {
next()
}

return () => {
isMounted = false
}
}, [inView, hasMore, next])

return <InfiniteScrollProvider ref={ref}>{children}</InfiniteScrollProvider>
Expand Down
19 changes: 19 additions & 0 deletions src/logic/contracts/__tests__/safeContractErrors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { decodeContractError } from '../safeContractErrors'

describe('decodeContractError', () => {
it('returns safe errors', () => {
expect(decodeContractError('GS000: Could not finish initialization')).toBe('GS000: Could not finish initialization')
})

it('returns safe errors irregardless of place in error', () => {
expect(decodeContractError('testGS000test')).toBe('GS000: Could not finish initialization')
expect(decodeContractError('test GS000 test')).toBe('GS000: Could not finish initialization')
})
it('returns safe errors irregardless of case', () => {
expect(decodeContractError('gs000: testing')).toBe('GS000: Could not finish initialization')
})

it('returns provided errors if not safe errors', () => {
expect(decodeContractError('Not a Safe error')).toBe('Not a Safe error')
})
})
45 changes: 45 additions & 0 deletions src/logic/contracts/contracts.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// https://github.com/gnosis/safe-contracts/blob/main/docs/error_codes.md
export enum CONTRACT_ERRORS {
// General init related
GS000 = 'Could not finish initialization',
GS001 = 'Threshold needs to be defined',

// General gas/ execution related
GS010 = 'Not enough gas to execute Safe transaction',
GS011 = 'Could not pay gas costs with ether',
GS012 = 'Could not pay gas costs with token',
GS013 = 'Safe transaction failed when gasPrice and safeTxGas were 0',

// General signature validation related
GS020 = 'Signatures data too short',
GS021 = 'Invalid contract signature location = inside static part',
GS022 = 'Invalid contract signature location = length not present',
GS023 = 'Invalid contract signature location = data not complete',
GS024 = 'Invalid contract signature provided',
GS025 = 'Hash has not been approved',
GS026 = 'Invalid owner provided',

// General auth related
GS030 = 'Only owners can approve a hash',
GS031 = 'Method can only be called from this contract',

// Module management related
GS100 = 'Modules have already been initialized',
GS101 = 'Invalid module address provided',
GS102 = 'Module has already been added',
GS103 = 'Invalid prevModule, module pair provided',
GS104 = 'Method can only be called from an enabled module',

// Owner management related
GS200 = 'Owners have already been setup',
GS201 = 'Threshold cannot exceed owner count',
GS202 = 'Threshold needs to be greater than 0',
GS203 = 'Invalid owner address provided',
GS204 = 'Address is already an owner',
GS205 = 'Invalid prevOwner, owner pair provided',

// Guard management related
GS300 = 'Guard does not implement IERC165',
}

export const CONTRACT_ERROR_CODES = Object.keys(CONTRACT_ERRORS)
43 changes: 43 additions & 0 deletions src/logic/contracts/safeContractErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import abi from 'ethereumjs-abi'

import { CONTRACT_ERRORS, CONTRACT_ERROR_CODES } from 'src/logic/contracts/contracts.d'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { GnosisSafe } from 'src/types/contracts/gnosis_safe.d'

export const decodeContractError = (contractError: string): string => {
const code = CONTRACT_ERROR_CODES.find((code) => {
return contractError.toUpperCase().includes(code.toUpperCase())
})

return code ? `${code}: ${CONTRACT_ERRORS[code]}` : contractError
}

export const getContractErrorMessage = async ({
safeInstance,
from,
data,
}: {
safeInstance: GnosisSafe
from: string
data: string
}): Promise<string> => {
const web3 = getWeb3()

let contractError: string

try {
const returnData = await web3.eth.call({
to: safeInstance.options.address,
from,
value: 0,
data,
})

const returnBuffer = Buffer.from(returnData.slice(2), 'hex')
contractError = abi.rawDecode(['string'], returnBuffer.slice(4))[0]
} catch (e) {
contractError = e.message
Copy link
Member

@katspaugh katspaugh Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should return null in this case?
And display the original tx err.

}

return decodeContractError(contractError)
}
36 changes: 19 additions & 17 deletions src/logic/safe/store/actions/createTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { generateSafeTxHash } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import fetchTransactions from './transactions/fetchTransactions'
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { PayableTx } from 'src/types/contracts/types.d'
Expand All @@ -35,6 +34,7 @@ import { currentChainId } from 'src/logic/config/store/selectors'
import { generateSafeRoute, history, SAFE_ROUTES } from 'src/routes/routes'
import { getCurrentShortChainName, getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { getContractErrorMessage } from 'src/logic/contracts/safeContractErrors'

export interface CreateTransactionArgs {
navigateToTransactionsTab?: boolean
Expand Down Expand Up @@ -191,30 +191,32 @@ export const createTransaction =
})
} catch (err) {
onError?.()
logError(Errors._803, err.message)

dispatch(closeSnackbarAction({ key: beforeExecutionKey }))

const executeDataUsedSignatures = safeInstance.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()

const contractErrorMessage = await getContractErrorMessage({
safeInstance,
from,
data: executeDataUsedSignatures,
})

logError(Errors._803, contractErrorMessage)

const notification = isTxPendingError(err)
? NOTIFICATIONS.TX_PENDING_MSG
: {
...notificationsQueue.afterExecutionError,
message: `${notificationsQueue.afterExecutionError.message} - ${err.message}`,
...(contractErrorMessage && {
message: `${notificationsQueue.afterExecutionError.message} - ${contractErrorMessage}`,
}),
}

dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
dispatch(enqueueSnackbar({ key: err.code, ...notification }))

logError(Errors._803, err.message)

if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
const executeDataUsedSignatures = safeInstance.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
try {
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeDataUsedSignatures, from)
logError(Errors._803, errMsg)
} catch (e) {
logError(Errors._803, e.message)
}
}
}

return txHash
Expand Down
40 changes: 20 additions & 20 deletions src/logic/safe/store/actions/processTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,17 @@ import { fetchSafe } from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { AppReduxState } from 'src/store'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'

import { Dispatch, DispatchReturn } from './types'
import { PayableTx } from 'src/types/contracts/types'

import { updateTransactionStatus } from 'src/logic/safe/store/actions/updateTransactionStatus'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import { Operation, TransactionStatus } from '@gnosis.pm/safe-react-gateway-sdk'
import { isTxPendingError } from 'src/logic/wallets/getWeb3'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { getContractErrorMessage } from 'src/logic/contracts/safeContractErrors'

interface ProcessTransactionArgs {
approveAndExecute: boolean
Expand Down Expand Up @@ -203,15 +201,9 @@ export const processTransaction =
return receipt.transactionHash
})
} catch (err) {
const notification = isTxPendingError(err)
? NOTIFICATIONS.TX_PENDING_MSG
: {
...notificationsQueue.afterExecutionError,
message: `${notificationsQueue.afterExecutionError.message} - ${err.message}`,
}
logError(Errors._804, err.message)

dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
dispatch(enqueueSnackbar({ key: err.code, ...notification }))

dispatch(
updateTransactionStatus({
Expand All @@ -223,17 +215,25 @@ export const processTransaction =
}),
)

logError(Errors._804, err.message)
const executeData = safeInstance.methods.approveHash(txHash).encodeABI()
const contractErrorMessage = await getContractErrorMessage({
safeInstance,
from,
data: executeData,
})

if (txHash) {
const executeData = safeInstance.methods.approveHash(txHash).encodeABI()
try {
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeData, from)
logError(Errors._804, errMsg)
} catch (e) {
logError(Errors._804, e.message)
}
}
logError(Errors._804, contractErrorMessage)

const notification = isTxPendingError(err)
? NOTIFICATIONS.TX_PENDING_MSG
: {
...notificationsQueue.afterExecutionError,
...(contractErrorMessage && {
message: `${notificationsQueue.afterExecutionError.message} - ${contractErrorMessage}`,
}),
}

dispatch(enqueueSnackbar({ key: err.code, ...notification }))
}

return txHash
Expand Down
3 changes: 2 additions & 1 deletion src/logic/safe/utils/aboutToExecuteTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { HistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
import { SafesMap } from 'src/logic/safe/store/reducer/types/safe'
import { Notification } from 'src/logic/notifications/notificationTypes'

let nonce: number | undefined

Expand All @@ -17,7 +18,7 @@ export const getNotification = (
{ safeAddress, values }: HistoryPayload,
userAddress: string,
safes: SafesMap,
): undefined => {
): undefined | Notification => {
const currentSafe = safes.get(safeAddress)

// no notification if not in the current safe or if its not an owner
Expand Down
4 changes: 3 additions & 1 deletion src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ const Routes = (): React.ReactElement => {
? location.pathname.replace(getPrefixedSafeAddressSlug(), 'SAFE_ADDRESS')
: location.pathname
trackPage(pathname + location.search)
}, [location, trackPage])

// Track when pathname changes
}, [location.pathname, trackPage])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Please add the search too.


return (
<Switch>
Expand Down
16 changes: 0 additions & 16 deletions src/test/utils/ethereumErrors.ts

This file was deleted.