Skip to content

Commit 9a29706

Browse files
fix(statics): skip ams tokens that duplicate an existing contract address
createTokenMapUsingConfigDetails merges dynamic AMS token configs into the statics coin map. The presence guard (isCoinPresentInCoinMap) dedupes only by name, id, and alias, but CoinMap.fromCoins also dedupes by contract address (family:networkType:contractAddress) and throws DuplicateContractAddressDefinitionError on a collision. A dynamic AMS token that shares a contract address with a statics token under a different name -- e.g. drift between the generated botTokens snapshot and live AMS data (eth:at on hoodi testnet) -- slipped past the name guard and threw in fromCoins, aborting the entire token-map build for every consumer (bitgo-retail hydration, bitgo-admin, bitgo-microservices, etc.). Track contract-address keys while merging and skip a duplicate with a warning, mirroring the existing malformed-token handling, so one bad token can no longer take down the whole map.
1 parent edc3d2e commit 9a29706

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

modules/statics/src/coins.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
bscToken,
99
celoToken,
1010
cosmosToken,
11+
ContractAddressDefinedToken,
1112
eosToken,
1213
erc20,
1314
hederaToken,
@@ -463,9 +464,14 @@ export function createTokenMapUsingConfigDetails(tokenConfigMap: Record<string,
463464
'terc1155:soneiumtoken',
464465
]);
465466

466-
// Add all the coins from statics coin map first
467+
// Add all the coins from statics coin map first, tracking the contract-address keys
468+
// they occupy (family:networkType:contractAddress, matching CoinMap.addCoin).
469+
const seenContractAddressKeys = new Set<string>();
467470
coins.forEach((coin, coinName) => {
468471
BaseCoins.set(coinName, coin);
472+
if (coin instanceof ContractAddressDefinedToken) {
473+
seenContractAddressKeys.add(`${coin.family}:${coin.network.type}:${coin.contractAddress}`);
474+
}
469475
});
470476

471477
// add the tokens not present in the static coin map
@@ -477,6 +483,22 @@ export function createTokenMapUsingConfigDetails(tokenConfigMap: Record<string,
477483
try {
478484
const token = createToken(tokenConfig);
479485
if (token) {
486+
// The name/id/alias guard above can miss a dynamic AMS token that shares a
487+
// contract address with a statics token (or another AMS token) under a different
488+
// name -- e.g. drift between the generated botTokens snapshot and live AMS data.
489+
// CoinMap.fromCoins dedupes by contract address and would throw
490+
// DuplicateContractAddressDefinitionError, aborting the entire token-map build.
491+
// Skip the duplicate with a warning instead, mirroring the malformed-token handling.
492+
if (token instanceof ContractAddressDefinedToken) {
493+
const contractAddressKey = `${token.family}:${token.network.type}:${token.contractAddress}`;
494+
if (seenContractAddressKeys.has(contractAddressKey)) {
495+
console.warn(
496+
`Skipping token with duplicate contract address: name="${token.name}" id="${token.id}" key="${contractAddressKey}"`
497+
);
498+
continue;
499+
}
500+
seenContractAddressKeys.add(contractAddressKey);
501+
}
480502
BaseCoins.set(token.name, token);
481503
}
482504
} catch (e) {

modules/statics/test/unit/coins.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,34 @@ describe('create token map using config details', () => {
13201320
tokenMap.has('hteth:faketoken').should.eql(false);
13211321
});
13221322

1323+
it('should skip an ams token that duplicates an existing contract address instead of throwing', () => {
1324+
const baseConfig = amsTokenConfigWithCustomToken['hteth:faketoken'][0];
1325+
// Two ams tokens with distinct names/ids but the same family + network type + contract address.
1326+
// The name/id/alias guard does not catch this, so without contract-address dedup the second
1327+
// token would throw DuplicateContractAddressDefinitionError in CoinMap.fromCoins and abort the
1328+
// entire token-map build (the failure observed via syncAmsCoinsToPresenter in bitgo-retail).
1329+
const tokenConfigWithDuplicateContract = {
1330+
'hteth:faketoken': [baseConfig],
1331+
'hteth:faketokendup': [
1332+
{
1333+
...baseConfig,
1334+
id: 'f0000000-0000-4000-8000-000000000001',
1335+
name: 'hteth:faketokendup',
1336+
asset: 'hteth:faketokendup',
1337+
},
1338+
],
1339+
};
1340+
1341+
let tokenMap: ReturnType<typeof createTokenMapUsingConfigDetails> | undefined;
1342+
(() => {
1343+
tokenMap = createTokenMapUsingConfigDetails(tokenConfigWithDuplicateContract);
1344+
}).should.not.throw();
1345+
1346+
// The first token wins; the contract-address duplicate is skipped.
1347+
tokenMap!.has('hteth:faketoken').should.eql(true);
1348+
tokenMap!.has('hteth:faketokendup').should.eql(false);
1349+
});
1350+
13231351
it('should create a coin map using reduced token config details', () => {
13241352
const coinMap1 = createTokenMapUsingTrimmedConfigDetails(reducedAmsTokenConfig);
13251353
const amsToken1 = coinMap1.get('hteth:faketoken');

0 commit comments

Comments
 (0)