Normalize erc20 token addresses and fix duplicate tokens (#2884)
What changed, and why it matters
This commit fixes a bookkeeping problem in Cake Wallet's handling of Ethereum-compatible tokens. Because Ethereum addresses are case-insensitive but the app was storing them with mixed-case keys, the same token could appear twice or get out of sync. The patch adds a one-time cleanup that converts all stored token addresses to lowercase and merges duplicates. It also updates the built-in token lists to use lowercase addresses. This is a data-consistency bug fix rather than an active remote hack, but if left unfixed it could have led to wrong balances, missing tokens, or in a worst-case scenario sending funds to or trusting the wrong contract address.
Treat as a defensive data-integrity fix. Review whether any other code paths still use mixed-case addresses as lookup keys (transaction history, balance fetchers, swap providers, address-book entries) and normalize them consistently. Add checksum (EIP-55) validation when users import custom tokens so maliciously cased addresses cannot be used to spoof legitimate tokens. Verify the migration handles concurrent wallet opens safely and that the `isPotentialScam` OR-merge does not accidentally preserve a scam flag on a legitimate token entry.
Security signals we found
Data normalization of case-sensitive keys for case-insensitive identifiers
Duplicate-token merge logic that ORs `enabled` and `isPotentialScam` flags
One-time migration guarded by a SharedPreferences flag
Hardcoded token contract addresses changed to lowercase across five chain registries
No input validation or checksum verification added for user-added tokens
Evidence from the diff
The change introduces _normalizeEvmChainErc20TokensBoxKeys() in EVMChainWalletBase, which runs once per wallet/chain. It iterates the Hive box of ERC-20 tokens, lowercases each key, rebuilds each Erc20Token with a lowercased contractAddress, merges duplicates by OR-ing enabled and isPotentialScam and preferring non-empty icon paths, then clears and rewrites the box. A SharedPreferences flag prevents re-running. The remaining diffs lower-case all hardcoded contract addresses in the Arbitrum, Base, BSC, Ethereum, and Polygon token registries. The bug being fixed is that EVM addresses are semantically case-insensitive (checksum casing is only for display/validation), so mixed-case storage keys created duplicate entries and inconsistent lookups.
Changed components
cw_evm/lib/evm_chain_wallet.dartcw_evm/lib/tokens/arbitrum_tokens.dartcw_evm/lib/tokens/base_tokens.dartcw_evm/lib/tokens/bsc_tokens.dartcw_evm/lib/tokens/ethereum_tokens.dartcw_evm/lib/tokens/polygon_tokens.dartInspect captured patch +119 / −35
diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart
index f064086b..ad81272f 100644
--- a/cw_evm/lib/evm_chain_wallet.dart
+++ b/cw_evm/lib/evm_chain_wallet.dart
@@ -276,6 +276,8 @@ abstract class EVMChainWalletBase
evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName);
}
+ await _normalizeEvmChainErc20TokensBoxKeys();
+
addInitialTokens();
}
@@ -306,6 +308,88 @@ abstract class EVMChainWalletBase
String getTransactionHistoryFileName() =>
EVMChainUtils.getTransactionHistoryFileName(selectedChainId);
+ /// Ensures all ERC20 token entries use lowercase contract addresses as
+ /// their Hive keys to avoid duplicates caused by case differences.
+ Future<void> _normalizeEvmChainErc20TokensBoxKeys() async {
+ if (!evmChainErc20TokensBox.isOpen) return;
+
+ final prefs = await sharedPrefs.future;
+ final migrationKey = 'erc20_box_normalized_${walletInfo.name}_$selectedChainId';
+
+ if (prefs.getBool(migrationKey) ?? false) return;
+
+ final box = evmChainErc20TokensBox;
+ final keys = box.keys.cast<String>().toList(growable: false);
+
+ if (keys.isEmpty) {
+ await prefs.setBool(migrationKey, true);
+ return;
+ }
+
+ final Map<String, Erc20Token> normalizedTokens = {};
+ var needsRewrite = false;
+
+ for (final key in keys) {
+ final token = box.get(key);
+
+ if (token == null) {
+ needsRewrite = true;
+ continue;
+ }
+
+ final lowerKey = key.toLowerCase();
+
+ final Erc20Token normalizedToken =
+ token.contractAddress == token.contractAddress.toLowerCase()
+ ? token
+ : Erc20Token(
+ name: token.name,
+ symbol: token.symbol,
+ contractAddress: token.contractAddress.toLowerCase(),
+ decimal: token.decimal,
+ enabled: token.enabled,
+ iconPath: token.iconPath,
+ tag: token.tag,
+ isPotentialScam: token.isPotentialScam,
+ );
+
+ if (!needsRewrite && (lowerKey != key || identical(normalizedToken, token) == false)) {
+ needsRewrite = true;
+ }
+
+ final existing = normalizedTokens[lowerKey];
+
+ if (existing == null) {
+ normalizedTokens[lowerKey] = normalizedToken;
+ continue;
+ }
+
+ final merged = Erc20Token(
+ name: normalizedToken.name,
+ symbol: normalizedToken.symbol,
+ contractAddress: lowerKey,
+ decimal: normalizedToken.decimal,
+ enabled: normalizedToken.enabled || existing.enabled,
+ iconPath: (normalizedToken.iconPath?.isNotEmpty ?? false)
+ ? normalizedToken.iconPath
+ : existing.iconPath,
+ tag: normalizedToken.tag ?? existing.tag,
+ isPotentialScam: normalizedToken.isPotentialScam || existing.isPotentialScam,
+ );
+
+ normalizedTokens[lowerKey] = merged;
+ }
+
+ if (needsRewrite) {
+ await box.clear();
+ for (final entry in normalizedTokens.entries) {
+ await box.put(entry.key, entry.value);
+ }
+ }
+
+ await prefs.setBool(migrationKey, true);
+ }
+
Future<bool> checkIfScanProviderIsEnabled() async {
final key = EVMChainUtils.getScanProviderPreferenceKey(selectedChainId);
return (await sharedPrefs.future).getBool(key) ?? true;
diff --git a/cw_evm/lib/tokens/arbitrum_tokens.dart b/cw_evm/lib/tokens/arbitrum_tokens.dart
index 34470fdf..fb6aa770 100644
--- a/cw_evm/lib/tokens/arbitrum_tokens.dart
+++ b/cw_evm/lib/tokens/arbitrum_tokens.dart
@@ -8,56 +8,56 @@ class ArbitrumTokens {
Erc20Token(
name: "Arbitrum",
symbol: "ARB",
- contractAddress: "0x912CE59144191C1204E64559FE8253a0e49E6548",
+ contractAddress: "0x912ce59144191c1204e64559fe8253a0e49e6548",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "USD Coin",
symbol: "USDC",
- contractAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
+ contractAddress: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "USDC.e",
symbol: "USDC.e",
- contractAddress: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
+ contractAddress: "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "Wrapped BTC",
symbol: "WBTC",
- contractAddress: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f",
+ contractAddress: "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f",
decimal: 8,
enabled: true,
),
Erc20Token(
name: "Chainlink Token",
symbol: "LINK",
- contractAddress: "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4",
+ contractAddress: "0xf97f4df75117a78c1a5a0dbb814af92458539fb4",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Wrapped liquid staked Ether 2.0",
symbol: "wstETH",
- contractAddress: "0x0fBcbaEA96Ce0cF7Ee00A8c19c3ab6f5Dc8E1921",
+ contractAddress: "0x0fbcbaea96ce0cf7ee00a8c19c3ab6f5dc8e1921",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Wrapped Ether",
symbol: "WETH",
- contractAddress: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
+ contractAddress: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "DAI",
symbol: "DAI",
- contractAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1",
+ contractAddress: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1",
decimal: 18,
enabled: false,
),
diff --git a/cw_evm/lib/tokens/base_tokens.dart b/cw_evm/lib/tokens/base_tokens.dart
index 6169bc92..83f7b408 100644
--- a/cw_evm/lib/tokens/base_tokens.dart
+++ b/cw_evm/lib/tokens/base_tokens.dart
@@ -15,21 +15,21 @@ class BaseTokens {
Erc20Token(
name: "USDe",
symbol: "USDe",
- contractAddress: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34",
+ contractAddress: "0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Dai",
symbol: "DAI",
- contractAddress: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
+ contractAddress: "0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Bridged Tether USD",
symbol: "USDT",
- contractAddress: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
+ contractAddress: "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
decimal: 6,
enabled: true,
),
@@ -43,14 +43,14 @@ class BaseTokens {
Erc20Token(
name: "Wrapped BTC",
symbol: "WBTC",
- contractAddress: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c",
+ contractAddress: "0x0555e30da8f98308edb960aa94c0db47230d2b9c",
decimal: 8,
enabled: false,
),
Erc20Token(
name: "SPX6900",
symbol: "SPX",
- contractAddress: "0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C",
+ contractAddress: "0x50da645f148798f68ef2d7db7c1cb22a6819bb2c",
decimal: 8,
enabled: false,
),
diff --git a/cw_evm/lib/tokens/bsc_tokens.dart b/cw_evm/lib/tokens/bsc_tokens.dart
index 2e003f2f..67a5078f 100644
--- a/cw_evm/lib/tokens/bsc_tokens.dart
+++ b/cw_evm/lib/tokens/bsc_tokens.dart
@@ -8,42 +8,42 @@ class BSCTokens {
Erc20Token(
name: "USD Coin",
symbol: "USDC",
- contractAddress: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
+ contractAddress: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Ethereum",
symbol: "ETH",
- contractAddress: "0x2170Ed0880ac9A755fd29B2688956BD959F933F8",
+ contractAddress: "0x2170ed0880ac9a755fd29b2688956bd959f933f8",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Tether USD",
symbol: "USDT",
- contractAddress: "0x55d398326f99059fF775485246999027B3197955",
+ contractAddress: "0x55d398326f99059ff775485246999027b3197955",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "USDe",
symbol: "USDe",
- contractAddress: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34",
+ contractAddress: "0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "PancakeSwap Token",
symbol: "CAKE",
- contractAddress: "0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82",
+ contractAddress: "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Cardano Token",
symbol: "ADA",
- contractAddress: "0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47",
+ contractAddress: "0x3ee2200efb3400fabb9aacf31297cbdd1d435d47",
decimal: 18,
enabled: false,
),
@@ -57,14 +57,14 @@ class BSCTokens {
Erc20Token(
name: "Wrapped BTC",
symbol: "WBTC",
- contractAddress: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c",
+ contractAddress: "0x0555e30da8f98308edb960aa94c0db47230d2b9c",
decimal: 8,
enabled: false,
),
Erc20Token(
name: "Wrapped BNB",
symbol: "WBNB",
- contractAddress: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
+ contractAddress: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
decimal: 18,
enabled: false,
),
diff --git a/cw_evm/lib/tokens/ethereum_tokens.dart b/cw_evm/lib/tokens/ethereum_tokens.dart
index 33e18ec8..5f1f4b27 100644
--- a/cw_evm/lib/tokens/ethereum_tokens.dart
+++ b/cw_evm/lib/tokens/ethereum_tokens.dart
@@ -22,21 +22,21 @@ class EthereumTokens {
Erc20Token(
name: "Decentralized Euro",
symbol: "DEURO",
- contractAddress: "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea",
+ contractAddress: "0xba3f535bbcccca2a154b573ca6c5a49baae0a3ea",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Dai",
symbol: "DAI",
- contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
+ contractAddress: "0x6b175474e89094c44da98b954eedeac495271d0f",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Wrapped Ether",
symbol: "WETH",
- contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ contractAddress: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
decimal: 18,
enabled: false,
),
@@ -64,7 +64,7 @@ class EthereumTokens {
Erc20Token(
name: "Matic Token",
symbol: "MATIC",
- contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
+ contractAddress: "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
decimal: 18,
enabled: false,
),
@@ -85,7 +85,7 @@ class EthereumTokens {
Erc20Token(
name: "Tether Gold",
symbol: "XAUT",
- contractAddress: "0x68749665FF8D2d112Fa859AA293F07A622782F38",
+ contractAddress: "0x68749665ff8d2d112fa859aa293f07a622782f38",
decimal: 6,
enabled: false,
iconPath: "assets/images/xaut_icon.png",
diff --git a/cw_evm/lib/tokens/polygon_tokens.dart b/cw_evm/lib/tokens/polygon_tokens.dart
index 24957a17..c6aa67be 100644
--- a/cw_evm/lib/tokens/polygon_tokens.dart
+++ b/cw_evm/lib/tokens/polygon_tokens.dart
@@ -8,63 +8,63 @@ class PolygonTokens {
Erc20Token(
name: "Wrapped Ether",
symbol: "WETH",
- contractAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
+ contractAddress: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Tether USD (PoS)",
symbol: "USDT",
- contractAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
+ contractAddress: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "USD Coin",
symbol: "USDC",
- contractAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
+ contractAddress: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "USD Coin (POS)",
symbol: "USDC.e",
- contractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
+ contractAddress: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
decimal: 6,
enabled: true,
),
Erc20Token(
name: "Decentralized Euro",
symbol: "DEURO",
- contractAddress: "0xC2ff25dD99e467d2589b2c26EDd270F220F14E47",
+ contractAddress: "0xc2ff25dd99e467d2589b2c26edd270f220f14e47",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "Avalanche Token",
symbol: "AVAX",
- contractAddress: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b",
+ contractAddress: "0x2c89bbc92bd86f8075d1decc58c7f4e0107f286b",
decimal: 18,
enabled: false,
),
Erc20Token(
name: "Wrapped BTC (PoS)",
symbol: "WBTC",
- contractAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6",
+ contractAddress: "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6",
decimal: 8,
enabled: false,
),
Erc20Token(
name: "Dai (PoS)",
symbol: "DAI",
- contractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063",
+ contractAddress: "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063",
decimal: 18,
enabled: true,
),
Erc20Token(
name: "SHIBA INU (PoS)",
symbol: "SHIB",
- contractAddress: "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec",
+ contractAddress: "0x6f8a06447ff6fcf75d803135a7de15ce88c1d4ec",
decimal: 18,
enabled: false,
),
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.