Skip to content

Commit e67ac3a

Browse files
authored
Merge pull request #5587 from VenusProtocol/feat/improve-error-message
feat: improve contract error message
2 parents 095298d + 4267421 commit e67ac3a

22 files changed

Lines changed: 504 additions & 7 deletions

File tree

.changeset/four-eggs-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@venusprotocol/evm": minor
3+
---
4+
5+
feat: support display contract error message for details

apps/evm/src/components/Notice/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const Notice = ({
6868
{title && <p className="text-sm font-semibold">{title}</p>}
6969

7070
{!!description && (
71-
<p className={cn('text-xs', size === 'md' && 'md:text-sm')}>{description}</p>
71+
<div className={cn('text-xs', size === 'md' && 'md:text-sm')}>{description}</div>
7272
)}
7373
</div>
7474
</div>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { QuaternaryButton, cn } from '@venusprotocol/ui';
2+
import copyToClipboard from 'copy-to-clipboard';
3+
import { useState } from 'react';
4+
5+
import { useTranslation } from 'libs/translations';
6+
7+
export interface ContractErrorNoticeProps {
8+
friendlyPhrase?: string;
9+
errorName: string;
10+
signature?: string;
11+
rawMessage: string;
12+
}
13+
14+
export const ContractErrorNotice: React.FC<ContractErrorNoticeProps> = ({
15+
friendlyPhrase,
16+
errorName,
17+
signature,
18+
rawMessage,
19+
}) => {
20+
const { t } = useTranslation();
21+
const [isRawVisible, setIsRawVisible] = useState(false);
22+
23+
const hasFriendlyPhrase = !!friendlyPhrase;
24+
const headline = friendlyPhrase ?? t('contractErrors.notice.fallback');
25+
26+
return (
27+
<div className="space-y-2">
28+
<p className="text-xs text-white md:text-sm">{headline}</p>
29+
30+
{!hasFriendlyPhrase && (
31+
<div className="rounded-lg border border-dark-grey-hover bg-background/50 px-3 py-2 font-mono text-xs break-all">
32+
<span className="text-light-grey">{t('contractErrors.notice.errorLabel')}: </span>
33+
<span className="text-white">{errorName}</span>
34+
{signature && <span className="text-light-grey"> ({signature})</span>}
35+
</div>
36+
)}
37+
38+
<div className="flex flex-wrap gap-2">
39+
<QuaternaryButton size="xs" onClick={() => copyToClipboard(rawMessage)}>
40+
{t('contractErrors.notice.copyDetails')}
41+
</QuaternaryButton>
42+
<QuaternaryButton size="xs" onClick={() => setIsRawVisible(prev => !prev)}>
43+
{isRawVisible
44+
? t('contractErrors.notice.hideRawError')
45+
: t('contractErrors.notice.showRawError')}
46+
</QuaternaryButton>
47+
</div>
48+
49+
{isRawVisible && (
50+
<pre
51+
className={cn(
52+
'max-h-60 overflow-auto rounded-lg border border-dark-grey-hover bg-background/50 p-3',
53+
'font-mono text-xs text-white whitespace-pre-wrap break-all',
54+
)}
55+
>
56+
{rawMessage}
57+
</pre>
58+
)}
59+
</div>
60+
);
61+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { t } from 'libs/translations';
2+
3+
export const customErrorPhrases: Record<string, string> = {
4+
ActionPaused: t('contractErrors.actionPaused'),
5+
InsufficientLiquidity: t('contractErrors.insufficientLiquidity'),
6+
InsufficientCollateral: t('contractErrors.insufficientCollateral'),
7+
SupplyCapExceeded: t('contractErrors.supplyCapExceeded'),
8+
BorrowCapExceeded: t('contractErrors.borrowCapExceeded'),
9+
TooMuchRepay: t('contractErrors.tooMuchRepay'),
10+
SwapDeadlineExpire: t('contractErrors.swapDeadlineExpire'),
11+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SELECTOR_LENGTH = 10;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { Abi } from 'viem';
2+
3+
import {
4+
aavePoolAddressesProviderAbi,
5+
aaveUiPoolDataProviderAbi,
6+
aaveV3PoolAbi,
7+
erc20Abi,
8+
governorBravoDelegateAbi,
9+
isolatedPoolComptrollerAbi,
10+
jumpRateModelAbi,
11+
jumpRateModelV2Abi,
12+
legacyPoolComptrollerAbi,
13+
leverageManagerAbi,
14+
maximillionAbi,
15+
multicall3Abi,
16+
nativeTokenGatewayAbi,
17+
nexusAbi,
18+
nexusAccountFactoryAbi,
19+
nexusBoostrapAbi,
20+
omnichainGovernanceExecutorAbi,
21+
pancakePairV2Abi,
22+
pendlePtVaultAbi,
23+
poolLensAbi,
24+
poolRegistryAbi,
25+
primeAbi,
26+
relativePositionManagerAbi,
27+
resilientOracleAbi,
28+
rewardsDistributorAbi,
29+
swapRouterAbi,
30+
swapRouterV2Abi,
31+
vBep20Abi,
32+
vBnbAbi,
33+
vTreasuryAbi,
34+
vTreasuryV8Abi,
35+
vaiAbi,
36+
vaiControllerAbi,
37+
vaiVaultAbi,
38+
venusLensAbi,
39+
vrtAbi,
40+
vrtConverterAbi,
41+
xVSProxyOFTDestAbi,
42+
xVSProxyOFTSrcAbi,
43+
xvsAbi,
44+
xvsStoreAbi,
45+
xvsTokenOmnichainAbi,
46+
xvsVaultAbi,
47+
xvsVestingAbi,
48+
zyFiVaultAbi,
49+
} from 'libs/contracts';
50+
51+
// ABIs scanned to decode raw revert data when viem has not pre-decoded it.
52+
// Includes every contract the frontend may interact with (Venus + third-party).
53+
export const CONTRACT_ERROR_ABIS: Abi[] = [
54+
// Venus — core lending
55+
isolatedPoolComptrollerAbi,
56+
legacyPoolComptrollerAbi,
57+
vBep20Abi,
58+
vBnbAbi,
59+
vaiControllerAbi,
60+
poolLensAbi,
61+
poolRegistryAbi,
62+
venusLensAbi,
63+
// Venus — extras
64+
primeAbi,
65+
nativeTokenGatewayAbi,
66+
rewardsDistributorAbi,
67+
swapRouterAbi,
68+
swapRouterV2Abi as Abi, // generated ABI has a malformed constructor field
69+
leverageManagerAbi,
70+
relativePositionManagerAbi,
71+
pendlePtVaultAbi,
72+
resilientOracleAbi,
73+
maximillionAbi,
74+
// Venus — tokens
75+
vaiAbi,
76+
xvsAbi,
77+
vrtAbi,
78+
vrtConverterAbi,
79+
// Venus — vaults / staking
80+
vaiVaultAbi,
81+
xvsVaultAbi,
82+
xvsStoreAbi,
83+
xvsVestingAbi,
84+
zyFiVaultAbi,
85+
// Venus — treasury
86+
vTreasuryAbi,
87+
vTreasuryV8Abi,
88+
// Venus — interest rate models
89+
jumpRateModelAbi,
90+
jumpRateModelV2Abi,
91+
// Venus — governance
92+
governorBravoDelegateAbi,
93+
omnichainGovernanceExecutorAbi,
94+
// Venus — cross-chain
95+
xvsTokenOmnichainAbi,
96+
xVSProxyOFTDestAbi,
97+
xVSProxyOFTSrcAbi,
98+
// Third-party — smart accounts
99+
nexusAbi,
100+
nexusAccountFactoryAbi,
101+
nexusBoostrapAbi,
102+
// Third-party — DeFi
103+
aaveV3PoolAbi,
104+
aaveUiPoolDataProviderAbi,
105+
aavePoolAddressesProviderAbi,
106+
pancakePairV2Abi,
107+
// Generic
108+
erc20Abi,
109+
multicall3Abi,
110+
];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { type Hex, decodeErrorResult } from 'viem';
2+
3+
import type { ParsedContractError } from '../parseContractError';
4+
import { CONTRACT_ERROR_ABIS } from './constants';
5+
6+
export const decodeWithContractErrorAbis = (
7+
rawData: Hex,
8+
signature: Hex,
9+
): ParsedContractError | undefined => {
10+
for (const abi of CONTRACT_ERROR_ABIS) {
11+
try {
12+
const decoded = decodeErrorResult({ abi, data: rawData });
13+
return { errorName: decoded.errorName, args: decoded.args, signature };
14+
} catch {
15+
// selector not in this ABI
16+
}
17+
}
18+
return undefined;
19+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { BaseError } from 'viem';
2+
3+
import { displayNotification } from 'libs/notifications';
4+
5+
import { ContractErrorNotice } from '../ContractErrorNotice';
6+
import { customErrorPhrases } from '../customErrorPhrases';
7+
import { logError } from '../logError';
8+
import type { ParsedContractError } from './parseContractError';
9+
10+
export interface HandleContractErrorInput {
11+
error: BaseError;
12+
parsed: ParsedContractError;
13+
}
14+
15+
export const handleContractError = ({ error, parsed }: HandleContractErrorInput) => {
16+
const firstArg = parsed.args?.[0];
17+
const friendlyPhrase =
18+
parsed.errorName === 'Error' && typeof firstArg === 'string'
19+
? firstArg
20+
: customErrorPhrases[parsed.errorName];
21+
22+
displayNotification({
23+
variant: 'error',
24+
description: (
25+
<ContractErrorNotice
26+
friendlyPhrase={friendlyPhrase}
27+
errorName={parsed.errorName}
28+
signature={parsed.signature}
29+
rawMessage={error.message}
30+
/>
31+
),
32+
});
33+
logError(error);
34+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Hex } from 'viem';
2+
3+
import { SELECTOR_LENGTH } from '../constants';
4+
5+
export const isHexSelector = (value: unknown): value is Hex =>
6+
typeof value === 'string' && value.startsWith('0x') && value.length >= SELECTOR_LENGTH;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { BaseError, ContractFunctionRevertedError, encodeErrorResult } from 'viem';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { isolatedPoolComptrollerAbi } from 'libs/contracts';
5+
6+
import { parseContractError } from '..';
7+
8+
describe('parseContractError', () => {
9+
it('decodes a viem ContractFunctionRevertedError that already carries decoded data', () => {
10+
const data = encodeErrorResult({
11+
abi: isolatedPoolComptrollerAbi,
12+
errorName: 'ActionPaused',
13+
args: ['0xED827b80Bd838192EA95002C01B5c6dA8354219a', 1],
14+
});
15+
16+
const error = new ContractFunctionRevertedError({
17+
abi: isolatedPoolComptrollerAbi,
18+
data,
19+
functionName: 'preBorrowHook',
20+
});
21+
22+
const parsed = parseContractError(error);
23+
expect(parsed?.errorName).toBe('ActionPaused');
24+
expect(parsed?.args).toEqual(['0xED827b80Bd838192EA95002C01B5c6dA8354219a', 1]);
25+
});
26+
27+
it('decodes raw hex revert data nested in the cause chain (estimateGas path)', () => {
28+
const rawData = encodeErrorResult({
29+
abi: isolatedPoolComptrollerAbi,
30+
errorName: 'BorrowCapExceeded',
31+
args: ['0xED827b80Bd838192EA95002C01B5c6dA8354219a', 1000n],
32+
});
33+
34+
const error = new BaseError('execution reverted', {
35+
cause: { data: rawData } as unknown as Error,
36+
});
37+
38+
const parsed = parseContractError(error);
39+
expect(parsed?.errorName).toBe('BorrowCapExceeded');
40+
expect(parsed?.args?.[1]).toBe(1000n);
41+
});
42+
43+
it('returns UnknownContractError when the selector is not in any Venus ABI', () => {
44+
const error = new BaseError('execution reverted', {
45+
cause: { data: '0xdeadbeef' } as unknown as Error,
46+
});
47+
48+
const parsed = parseContractError(error);
49+
expect(parsed?.errorName).toBe('UnknownContractError');
50+
expect(parsed?.signature).toBe('0xdeadbeef');
51+
});
52+
53+
it('returns undefined for non-viem errors', () => {
54+
expect(parseContractError(new Error('random'))).toBeUndefined();
55+
expect(parseContractError(null)).toBeUndefined();
56+
expect(parseContractError('boom')).toBeUndefined();
57+
});
58+
});

0 commit comments

Comments
 (0)