Skip to content

Commit

Permalink
feat: malicious token filtering (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
meeh0w authored Jan 29, 2025
1 parent 8e6143a commit 4a00253
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 259 deletions.
35 changes: 18 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,26 @@
"sentry": "node sentryscript.js"
},
"dependencies": {
"@avalabs/avalanche-module": "0.12.0",
"@avalabs/avalanchejs": "4.1.2-alpha.1",
"@avalabs/bitcoin-module": "0.12.0",
"@avalabs/avalanche-module": "1.2.0",
"@avalabs/avalanchejs": "4.1.2-alpha.3",
"@avalabs/bitcoin-module": "1.2.0",
"@avalabs/bridge-unified": "4.0.1",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.27",
"@avalabs/core-chains-sdk": "3.1.0-alpha.27",
"@avalabs/core-coingecko-sdk": "3.1.0-alpha.27",
"@avalabs/core-covalent-sdk": "3.1.0-alpha.27",
"@avalabs/core-etherscan-sdk": "3.1.0-alpha.27",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.32",
"@avalabs/core-chains-sdk": "3.1.0-alpha.32",
"@avalabs/core-coingecko-sdk": "3.1.0-alpha.32",
"@avalabs/core-covalent-sdk": "3.1.0-alpha.32",
"@avalabs/core-etherscan-sdk": "3.1.0-alpha.32",
"@avalabs/core-k2-components": "4.18.0-alpha.53",
"@avalabs/core-snowtrace-sdk": "3.1.0-alpha.27",
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.27",
"@avalabs/core-utils-sdk": "3.1.0-alpha.27",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.27",
"@avalabs/evm-module": "0.12.0",
"@avalabs/glacier-sdk": "3.1.0-alpha.27",
"@avalabs/hvm-module": "1.0.0",
"@avalabs/core-snowtrace-sdk": "3.1.0-alpha.32",
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.32",
"@avalabs/core-utils-sdk": "3.1.0-alpha.32",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.32",
"@avalabs/evm-module": "1.2.0",
"@avalabs/glacier-sdk": "3.1.0-alpha.32",
"@avalabs/hw-app-avalanche": "0.14.1",
"@avalabs/types": "3.1.0-alpha.27",
"@avalabs/vm-module-types": "0.12.0",
"@avalabs/hvm-module": "1.2.0",
"@avalabs/types": "3.1.0-alpha.32",
"@avalabs/vm-module-types": "1.2.0",
"@blockaid/client": "0.10.0",
"@coinbase/cbpay-js": "1.6.0",
"@cubist-labs/cubesigner-sdk": "0.3.28",
Expand Down Expand Up @@ -267,6 +267,7 @@
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>@ledgerhq/hw-app-btc>bitcoinjs-lib>bip32>tiny-secp256k1": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>@avalabs/core-wallets-sdk>hdkey>secp256k1": false,
"@avalabs/core-bridge-sdk>@avalabs/core-wallets-sdk>@metamask/eth-sig-util>@metamask/utils>@ethereumjs/tx>@ethereumjs/common>ethereumjs-util>ethereum-cryptography>keccak": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>bufferutil": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>@metamask/sdk-communication-layer>utf-8-validate": false,
"@avalabs/avalanche-module>@avalabs/vm-module-types>hypersdk-client>@metamask/sdk>eciesjs>secp256k1": false
Expand Down
2 changes: 2 additions & 0 deletions src/background/utils/findToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const UNKNOWN_TOKEN = (address: string): TokenWithBalanceERC20 => ({
balance: 0n,
balanceDisplayValue: '0',
decimals: 0,
reputation: null,
});

export async function findToken(
Expand Down Expand Up @@ -106,5 +107,6 @@ export async function findToken(
balance: balance,
balanceDisplayValue: bigIntToString(balance, tokenData.decimals),
type: TokenType.ERC20,
reputation: null,
};
}
36 changes: 36 additions & 0 deletions src/components/common/MaliciousTokenWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
AlertTriangleIcon,
Box,
BoxProps,
Tooltip,
useTheme,
} from '@avalabs/core-k2-components';
import { useTranslation } from 'react-i18next';

import { WarningBox } from '@src/pages/Permissions/components/WarningBox';

export const MaliciousTokenWarningBox = (props: BoxProps) => {
const { t } = useTranslation();

return (
<Box {...props}>
<WarningBox
title={t('Malicious Token')}
text={t(
'This token has been flagged as malicious. Use caution when interacting with it.',
)}
/>
</Box>
);
};

export const MaliciousTokenWarningIcon = ({ size }: { size?: number }) => {
const { t } = useTranslation();
const theme = useTheme();

return (
<Tooltip title={t('This token has been flagged as malicious')}>
<AlertTriangleIcon color={theme.palette.warning.main} size={size ?? 16} />
</Tooltip>
);
};
9 changes: 7 additions & 2 deletions src/components/common/TokenCardWithBalance.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Card, Stack, Tooltip, Typography } from '@avalabs/core-k2-components';
import { useEffect, useRef, useState } from 'react';
import { PAndL } from './ProfitAndLoss';
import { TokenWithBalance } from '@avalabs/vm-module-types';

import { PAndL } from './ProfitAndLoss';
import { MaliciousTokenWarningIcon } from './MaliciousTokenWarning';

interface TokenCardProps {
name: string;
symbol: string;
Expand All @@ -13,6 +15,7 @@ interface TokenCardProps {
currencyFormatter?: (balanceInCurrency: number) => string;
currency?: string;
priceChanges?: TokenWithBalance['priceChanges'];
isMalicious?: boolean;
}

export function TokenCardWithBalance({
Expand All @@ -25,6 +28,7 @@ export function TokenCardWithBalance({
currencyFormatter,
currency,
priceChanges,
isMalicious,
}: TokenCardProps) {
const [hasNameOverflow, setHasNameOverflow] = useState(false);

Expand Down Expand Up @@ -68,8 +72,9 @@ export function TokenCardWithBalance({
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ width: '100%' }}
sx={{ width: '100%', gap: 1 }}
>
{isMalicious && <MaliciousTokenWarningIcon />}
<Tooltip
placement="bottom"
title={<Typography variant="caption">{name}</Typography>}
Expand Down
12 changes: 7 additions & 5 deletions src/contexts/SettingsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
TokenType,
TokenWithBalance,
} from '@avalabs/vm-module-types';
import { isTokenMalicious } from '@src/utils/isTokenMalicious';

type SettingsFromProvider = SettingsState & {
lockWallet(): Promise<true>;
Expand Down Expand Up @@ -128,10 +129,7 @@ export function SettingsContextProvider({ children }: { children: any }) {
params: [
{
...tokensVisibility,
[key]:
tokensVisibility[key] !== undefined
? !tokensVisibility[key]
: false,
[key]: !getTokenVisibility(token),
},
],
});
Expand All @@ -141,7 +139,11 @@ export function SettingsContextProvider({ children }: { children: any }) {
(token: TokenWithBalance) => {
const key = token.type === TokenType.ERC20 ? token.address : token.symbol;
const tokensVisibility = settings?.tokensVisibility ?? {};
return tokensVisibility[key] || tokensVisibility[key] === undefined;

// If the token is flagged as malicious, only show it if the user specifcially enabled it.
return isTokenMalicious(token)
? tokensVisibility[key]
: tokensVisibility[key] || tokensVisibility[key] === undefined;
},
[settings?.tokensVisibility],
);
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useTokensWithBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const useTokensWithBalances = (
type: TokenType.ERC20,
balance: 0n,
balanceDisplayValue: '0',
reputation: null,
};

return acc;
Expand Down Expand Up @@ -106,6 +107,7 @@ export const useTokensWithBalances = (
type: TokenType.ERC20,
balance: 0n,
balanceDisplayValue: '0',
reputation: null,
};

return tokensWithBalances;
Expand Down
3 changes: 3 additions & 0 deletions src/localization/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@
"Looks like you got here by accident.": "Looks like you got here by accident.",
"MFA configuration is required for your account.": "MFA configuration is required for your account.",
"Malicious Application": "Malicious Application",
"Malicious Token": "Malicious Token",
"Manage": "Manage",
"Manage Collectibles": "Manage Collectibles",
"Manage Networks": "Manage Networks",
Expand Down Expand Up @@ -883,6 +884,8 @@
"This password was set when you created the keystore file.": "This password was set when you created the keystore file.",
"This recovery phrase appears to have already been imported.": "This recovery phrase appears to have already been imported.",
"This recovery phrase is already imported.": "This recovery phrase is already imported.",
"This token has been flagged as malicious": "This token has been flagged as malicious",
"This token has been flagged as malicious. Use caution when interacting with it.": "This token has been flagged as malicious. Use caution when interacting with it.",
"This transaction is malicious do not proceed.": "This transaction is malicious do not proceed.",
"This transaction requires multiple approvals.": "This transaction requires multiple approvals.",
"This transaction requires two approvals": "This transaction requires two approvals",
Expand Down
1 change: 1 addition & 0 deletions src/pages/Bridge/utils/findTokenForAsset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('src/pages/Bridge/utils/findTokenForAsset', () => {
name: 'Wrapped ETH',
symbol: 'WETH.e',
decimals: 1,
reputation: null,
};

it('returns wrapped token for ethereum', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/pages/Home/components/Portfolio/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TokenIcon } from '@src/components/common/TokenIcon';
import { normalizeBalance } from '@src/utils/normalizeBalance';
import Big from 'big.js';
import { TokenType } from '@avalabs/vm-module-types';
import { isTokenMalicious } from '@src/utils/isTokenMalicious';

const TokenRow = styled('div')`
padding: 0 10px 0 16px;
Expand Down Expand Up @@ -97,6 +98,7 @@ export function TokenList({ searchQuery }: TokenListProps) {
balanceDisplayValue={token.balanceDisplayValue}
balanceInCurrency={token.balanceInCurrency?.toString()}
priceChanges={token.priceChanges}
isMalicious={isTokenMalicious(token)}
>
<TokenIcon
width="32px"
Expand Down
3 changes: 3 additions & 0 deletions src/pages/Home/components/Portfolio/TokenListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface TokenListItemProps {
percentage?: number | undefined;
value?: number | undefined;
};
isMalicious?: boolean;
}

export function TokenListItem({
Expand All @@ -22,6 +23,7 @@ export function TokenListItem({
balanceInCurrency,
onClick,
priceChanges,
isMalicious,
}: TokenListItemProps) {
const { currencyFormatter } = useSettingsContext();
return (
Expand All @@ -33,6 +35,7 @@ export function TokenListItem({
balanceInCurrency={balanceInCurrency}
currencyFormatter={currencyFormatter}
priceChanges={priceChanges}
isMalicious={isMalicious}
>
{children}
</TokenCardWithBalance>
Expand Down
81 changes: 45 additions & 36 deletions src/pages/ManageTokens/AddToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
} from '@avalabs/core-k2-components';
import { TokenCardWithBalance } from '@src/components/common/TokenCardWithBalance';
import { TokenIcon } from '@src/components/common/TokenIcon';
import { TokenType } from '@avalabs/vm-module-types';
import { TokenType, TokenWithBalanceERC20 } from '@avalabs/vm-module-types';
import { isTokenMalicious } from '@src/utils/isTokenMalicious';
import { NetworkContractToken } from '@avalabs/core-chains-sdk';
import { MaliciousTokenWarningBox } from '@src/components/common/MaliciousTokenWarning';

export function AddToken() {
const { t } = useTranslation();
Expand All @@ -31,10 +34,7 @@ export function AddToken() {

const [addressInput, setAddressInput] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tokenData, setTokenData] = useState<{
name: string;
symbol: string;
} | null>(null);
const [newTokenData, setNewTokenData] = useState<NetworkContractToken>();
const [error, setError] = useState<string>('');
const { capture } = useAnalyticsContext();

Expand Down Expand Up @@ -62,44 +62,49 @@ export function AddToken() {
}
};

const tokenAlreadyExists = useMemo(
const existingToken = useMemo(
() =>
addressInput?.length &&
tokens.some(
tokens.find(
(token) =>
token.type === TokenType.ERC20 &&
token.address.toLowerCase() === addressInput.toLowerCase(),
),
) as TokenWithBalanceERC20 | undefined,
[tokens, addressInput],
);

useEffect(() => {
if (!addressInput?.length) {
setTokenData(null);
if (!addressInput.length) {
setNewTokenData(undefined);
setError('');
return;
}

const getTokenData = async () => {
if (existingToken) {
setError(t('Token already exists in your wallet.'));
return;
}

setIsLoading(true);
const data = await request<GetTokenDataHandler>({
method: ExtensionRequest.SETTINGS_GET_TOKEN_DATA,
params: [addressInput],
});
setIsLoading(false);
setTokenData(data || null);
setNewTokenData(undefined);

let errorMessage = '';
if (!data) {
errorMessage = t('Not a valid ERC-20 token address.');
}
if (tokenAlreadyExists) {
errorMessage = t('Token already exists in your wallet.');
setError(t('Not a valid ERC-20 token address.'));
}
setError(errorMessage);
};

getTokenData();
}, [request, addressInput, network, tokenAlreadyExists, t]);
}, [request, addressInput, network, existingToken, t]);

const tokenData = useMemo(
() => existingToken ?? newTokenData,
[existingToken, newTokenData],
);

return (
<>
Expand Down Expand Up @@ -130,22 +135,26 @@ export function AddToken() {
helperText={error}
/>
{tokenData && (
<Stack sx={{ mt: 5, rowGap: 1 }}>
<Typography
variant="body2"
sx={{
fontWeight: 'fontWeightSemibold',
}}
>
{t('Token')}
</Typography>
<TokenCardWithBalance
name={tokenData.name}
symbol={tokenData.symbol}
>
<TokenIcon width="32px" height="32px" name={tokenData.name} />
</TokenCardWithBalance>
</Stack>
<>
<Stack sx={{ mt: 5, rowGap: 1 }}>
<Typography
variant="body2"
sx={{
fontWeight: 'fontWeightSemibold',
}}
>
{t('Token')}
</Typography>
{isTokenMalicious(tokenData) && <MaliciousTokenWarningBox />}
<TokenCardWithBalance
name={tokenData.name}
symbol={tokenData.symbol}
isMalicious={isTokenMalicious(tokenData)}
>
<TokenIcon width="32px" height="32px" name={tokenData.name} />
</TokenCardWithBalance>
</Stack>
</>
)}
<Stack
sx={{
Expand All @@ -157,7 +166,7 @@ export function AddToken() {
<Button
data-testid="add-custom-token-button"
onClick={addCustomToken}
disabled={isLoading || !!error?.length || !tokenData}
disabled={isLoading || !!error?.length || !newTokenData}
fullWidth
size="large"
>
Expand Down
Loading

0 comments on commit 4a00253

Please sign in to comment.