Skip to content

Commit c72660c

Browse files
authored
Show conflicting proxy implementations warning (#3179)
* Show conflicting proxy implementations warning Resolves #2884 * fix: add conflicting_implementations field to contract mocks * fix tests * proxy info tweaks * fix ts
1 parent e454b4b commit c72660c

File tree

33 files changed

+369
-160
lines changed

33 files changed

+369
-160
lines changed

mocks/contract/info.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const verified: SmartContract = {
4343
file_path: '',
4444
additional_sources: [],
4545
verified_twin_address_hash: null,
46+
conflicting_implementations: null,
4647
};
4748

4849
export const certified: SmartContract = {
@@ -144,6 +145,7 @@ export const nonVerified: SmartContract = {
144145
additional_sources: [],
145146
external_libraries: null,
146147
verified_twin_address_hash: null,
148+
conflicting_implementations: null,
147149
language: null,
148150
license_type: null,
149151
};

types/api/contract.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Abi, AbiType } from 'abitype';
22

3+
import type { AddressImplementation } from './addressParams';
4+
35
export type SmartContractMethodArgType = AbiType;
46
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
57

@@ -24,6 +26,8 @@ export type SmartContractLicenseType =
2426
export type SmartContractProxyType =
2527
'eip1167' |
2628
'eip1967' |
29+
'eip1967_oz' |
30+
'eip1967_beacon' |
2731
'eip1822' |
2832
'eip930' |
2933
'eip2535' |
@@ -38,6 +42,11 @@ export type SmartContractProxyType =
3842
'unknown' |
3943
null;
4044

45+
export interface SmartContractConflictingImplementation {
46+
proxy_type: NonNullable<SmartContractProxyType>;
47+
implementations: Array<AddressImplementation>;
48+
}
49+
4150
export interface SmartContract {
4251
deployed_bytecode: string | null;
4352
creation_bytecode: string | null;
@@ -53,6 +62,7 @@ export interface SmartContract {
5362
is_verified: boolean | null;
5463
is_verified_via_eth_bytecode_db: boolean | null;
5564
is_changed_bytecode: boolean | null;
65+
conflicting_implementations: Array<SmartContractConflictingImplementation> | null;
5666

5767
// sourcify info >>>
5868
is_verified_via_sourcify: boolean | null;

ui/address/AddressContract.pw.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test.describe('ABI functionality', () => {
3232
const socket = await createSocket();
3333
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
3434

35-
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible();
35+
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeVisible();
3636
await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click();
3737
await expect(component.getByLabel('FLASHLOAN_PREMIUM_TOTAL').getByRole('button', { name: 'Read' })).toBeVisible();
3838
});
@@ -48,7 +48,7 @@ test.describe('ABI functionality', () => {
4848
const socket = await createSocket();
4949
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
5050

51-
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden();
51+
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeHidden();
5252
await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click();
5353
await expect(component.getByLabel('FLASHLOAN_PREMIUM_TOTAL').getByRole('button', { name: 'Read' })).toBeVisible();
5454
});
@@ -63,7 +63,7 @@ test.describe('ABI functionality', () => {
6363
const socket = await createSocket();
6464
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
6565

66-
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible();
66+
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeVisible();
6767
await component.getByText('setReserveInterestRateStrategyAddress').click();
6868
await expect(component.getByLabel('9.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
6969
await expect(component.getByLabel('9.').getByRole('button', { name: 'Write' })).toBeEnabled();
@@ -85,7 +85,7 @@ test.describe('ABI functionality', () => {
8585
const socket = await createSocket();
8686
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
8787

88-
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden();
88+
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeHidden();
8989
await component.getByText('setReserveInterestRateStrategyAddress').click();
9090
await expect(component.getByLabel('9.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
9191
await expect(component.getByLabel('9.').getByRole('button', { name: 'Write' })).toBeDisabled();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Grid, GridItem, HStack, Text, VStack } from '@chakra-ui/react';
2+
import React from 'react';
3+
4+
import type { SmartContractConflictingImplementation } from 'types/api/contract';
5+
6+
import { Button } from 'toolkit/chakra/button';
7+
import { DialogActionTrigger, DialogBody, DialogContent, DialogHeader, DialogRoot, DialogTrigger } from 'toolkit/chakra/dialog';
8+
import { Link } from 'toolkit/chakra/link';
9+
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
10+
11+
import { PROXY_TYPES } from './utils';
12+
13+
interface Props {
14+
data: Array<SmartContractConflictingImplementation>;
15+
children: React.ReactNode;
16+
}
17+
18+
const ConflictingImplementationsModal = ({ data, children }: Props) => {
19+
return (
20+
<DialogRoot size={{ lgDown: 'full', lg: 'md' }}>
21+
<DialogTrigger>
22+
{ children }
23+
</DialogTrigger>
24+
<DialogContent>
25+
<DialogHeader>Detected proxy implementations</DialogHeader>
26+
<DialogBody>
27+
<Text>
28+
Multiple proxy patterns were detected for this contract.
29+
This may be due to an unsupported custom proxy design or due to a malicious proxy spoofing attempt.
30+
Review carefully.
31+
</Text>
32+
<VStack alignItems="stretch" mt={ 6 } textStyle="sm">
33+
{ data.map((item) => {
34+
const addressNum = item.implementations.length;
35+
const addressText = addressNum === 1 ? 'Implementation:' : 'Implementations:';
36+
const proxyType = PROXY_TYPES[item.proxy_type]?.name || PROXY_TYPES.unknown?.name;
37+
38+
return (
39+
<Grid
40+
key={ item.proxy_type }
41+
templateColumns="115px minmax(0px, 1fr)"
42+
w="100%"
43+
p={ 4 }
44+
columnGap={ 5 }
45+
rowGap={ 2 }
46+
borderRadius="md"
47+
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
48+
>
49+
<GridItem>Proxy type:</GridItem>
50+
<GridItem>{ proxyType }</GridItem>
51+
<GridItem>{ addressText }</GridItem>
52+
<GridItem>
53+
<VStack alignItems="stretch">
54+
{ item.implementations.map((implementation) => (
55+
<AddressEntity
56+
key={ implementation.address_hash }
57+
address={{ hash: implementation.address_hash, name: implementation.name }}
58+
/>
59+
)) }
60+
</VStack>
61+
</GridItem>
62+
</Grid>
63+
);
64+
}) }
65+
</VStack>
66+
<HStack mt={ 6 } gap={ 6 }>
67+
<DialogActionTrigger asChild>
68+
<Button>Got it, thanks</Button>
69+
</DialogActionTrigger>
70+
<Link external noIcon href="https://discord.com/invite/blockscout">
71+
Contact us
72+
</Link>
73+
</HStack>
74+
</DialogBody>
75+
</DialogContent>
76+
</DialogRoot>
77+
);
78+
};
79+
80+
export default React.memo(ConflictingImplementationsModal);

ui/address/contract/alerts/ContractDetailsAlertProxyPattern.pw.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,31 @@ test('proxy type without link', async({ render }) => {
1818
const component = await render(<ContractDetailsAlertProxyPattern type="basic_implementation" isLoading={ false }/>);
1919
await expect(component).toHaveScreenshot();
2020
});
21+
22+
test('proxy type with conflicting implementations', async({ render, page }) => {
23+
const component = await render(
24+
<ContractDetailsAlertProxyPattern
25+
type="resolved_delegate_proxy"
26+
isLoading={ false }
27+
conflictingImplementations={ [
28+
{
29+
proxy_type: 'eip1967',
30+
implementations: [
31+
{ address_hash: '0x568AA6C21cCf558C47F2A01B60cc6D549cED2F59' },
32+
],
33+
},
34+
{
35+
proxy_type: 'eip1967_oz',
36+
implementations: [
37+
{ address_hash: '0x7d608aBCf9a3BE6B869E745E6F8dB3434877D60F', name: 'Implementation 3' },
38+
{ address_hash: '0x568AA6C21cCf558C47F2A01B60cc6D549cED2F59' },
39+
],
40+
},
41+
] }
42+
/>,
43+
);
44+
await expect(component).toHaveScreenshot();
45+
46+
page.getByText('View details').click();
47+
await expect(page).toHaveScreenshot();
48+
});

ui/address/contract/alerts/ContractDetailsAlertProxyPattern.tsx

Lines changed: 24 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,50 @@
1+
import { Box } from '@chakra-ui/react';
12
import React from 'react';
23

3-
import type { SmartContractProxyType } from 'types/api/contract';
4+
import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract';
45

56
import { Alert } from 'toolkit/chakra/alert';
67
import { Link } from 'toolkit/chakra/link';
8+
import { space } from 'toolkit/utils/htmlEntities';
9+
10+
import ConflictingImplementationsModal from './ConflictingImplementationsModal';
11+
import { PROXY_TYPES } from './utils';
712

813
interface Props {
914
type: NonNullable<SmartContractProxyType>;
10-
isLoading: boolean;
15+
isLoading?: boolean;
16+
conflictingImplementations?: Array<SmartContractConflictingImplementation>;
1117
}
1218

13-
const PROXY_TYPES: Partial<Record<NonNullable<SmartContractProxyType>, {
14-
name: string;
15-
link?: string;
16-
description?: string;
17-
}>> = {
18-
eip1167: {
19-
name: 'EIP-1167',
20-
link: 'https://eips.ethereum.org/EIPS/eip-1167',
21-
description: 'Minimal proxy',
22-
},
23-
eip1967: {
24-
name: 'EIP-1967',
25-
link: 'https://eips.ethereum.org/EIPS/eip-1967',
26-
description: 'Proxy storage slots',
27-
},
28-
eip1822: {
29-
name: 'EIP-1822',
30-
link: 'https://eips.ethereum.org/EIPS/eip-1822',
31-
description: 'Universal upgradeable proxy standard (UUPS)',
32-
},
33-
eip2535: {
34-
name: 'EIP-2535',
35-
link: 'https://eips.ethereum.org/EIPS/eip-2535',
36-
description: 'Diamond proxy',
37-
},
38-
eip930: {
39-
name: 'ERC-930',
40-
link: 'https://github.com/ethereum/EIPs/issues/930',
41-
description: 'Eternal storage',
42-
},
43-
erc7760: {
44-
name: 'ERC-7760',
45-
link: 'https://eips.ethereum.org/EIPS/eip-7760',
46-
description: 'Minimal Upgradeable Proxies',
47-
},
48-
resolved_delegate_proxy: {
49-
name: 'ResolvedDelegateProxy',
50-
// eslint-disable-next-line max-len
51-
link: 'https://github.com/ethereum-optimism/optimism/blob/9580179013a04b15e6213ae8aa8d43c3f559ed9a/packages/contracts-bedrock/src/legacy/ResolvedDelegateProxy.sol',
52-
description: 'OP stack: legacy proxy contract that makes use of the AddressManager to resolve the implementation address',
53-
},
54-
clone_with_immutable_arguments: {
55-
name: 'Clones with immutable arguments',
56-
link: 'https://github.com/wighawag/clones-with-immutable-args',
57-
},
58-
master_copy: {
59-
name: 'Safe proxy',
60-
link: 'https://github.com/safe-global/safe-smart-account',
61-
},
62-
comptroller: {
63-
name: 'Compound protocol proxy',
64-
link: 'https://github.com/compound-finance/compound-protocol',
65-
},
66-
basic_implementation: {
67-
name: 'public implementation() getter',
68-
},
69-
basic_get_implementation: {
70-
name: 'public getImplementation() getter',
71-
},
72-
unknown: {
73-
name: 'Unknown proxy pattern',
74-
},
75-
};
76-
77-
const ContractCodeProxyPattern = ({ type, isLoading }: Props) => {
19+
const ContractCodeProxyPattern = ({ type, isLoading, conflictingImplementations }: Props) => {
7820
const proxyInfo = PROXY_TYPES[type];
7921

8022
if (!proxyInfo || type === 'unknown') {
8123
return null;
8224
}
8325

26+
const status = conflictingImplementations && conflictingImplementations.length > 0 ? 'warning' : 'success';
27+
8428
return (
85-
<Alert status="warning" whiteSpace="pre-wrap" loading={ isLoading }>
29+
<Alert status={ status } whiteSpace="pre-wrap" loading={ isLoading } descriptionProps={{ flexDir: 'column' }}>
8630
{ proxyInfo.link ? (
87-
<>
31+
<Box>
8832
This proxy smart-contract is detected via <Link href={ proxyInfo.link } external>{ proxyInfo.name }</Link>
8933
{ proxyInfo.description && ` - ${ proxyInfo.description }` }
90-
</>
34+
</Box>
9135
) : (
92-
<>
36+
<Box>
9337
This proxy smart-contract is detected via { proxyInfo.name }
9438
{ proxyInfo.description && ` - ${ proxyInfo.description }` }
95-
</>
39+
</Box>
40+
) }
41+
{ conflictingImplementations && conflictingImplementations.length > 0 && (
42+
<Box mt={ 1 } whiteSpace="pre-wrap">
43+
<span>This contract contains more than one proxy implementation address.{ space }</span>
44+
<ConflictingImplementationsModal data={ conflictingImplementations }>
45+
<Link>View details</Link>
46+
</ConflictingImplementationsModal>
47+
</Box>
9648
) }
9749
</Alert>
9850
);

ui/address/contract/alerts/ContractDetailsAlerts.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ const ContractDetailsAlerts = ({ data, isLoading, addressData, channel }: Props)
6060
}
6161
</Alert>
6262
) }
63+
{ addressData.proxy_type && (
64+
<ContractDetailsAlertProxyPattern
65+
type={ addressData.proxy_type }
66+
isLoading={ isLoading }
67+
conflictingImplementations={ data?.conflicting_implementations ?? undefined }
68+
/>
69+
) }
6370
<ContractDetailsAlertVerificationSource data={ data }/>
6471
{ (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
6572
<Alert status="warning">
@@ -82,7 +89,6 @@ const ContractDetailsAlerts = ({ data, isLoading, addressData, channel }: Props)
8289
<span> page</span>
8390
</Alert>
8491
) }
85-
{ addressData.proxy_type && <ContractDetailsAlertProxyPattern type={ addressData.proxy_type } isLoading={ isLoading }/> }
8692
</Flex>
8793
);
8894
};
Loading
Loading
Loading

0 commit comments

Comments
 (0)