Skip to content

feat: automate ERC20 automation #6665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions modules/bitgo/src/v2/coinFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Near, TNear, Nep141Token } from '@bitgo/sdk-coin-near';
import { SolToken } from '@bitgo/sdk-coin-sol';
import { TrxToken } from '@bitgo/sdk-coin-trx';
import { CoinFactory, CoinConstructor } from '@bitgo/sdk-core';
import { EthLikeErc20Token } from '@bitgo/sdk-coin-evm';
import {
CoinMap,
coins,
Expand Down Expand Up @@ -532,6 +533,20 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
VetToken.createTokenConstructors().forEach(({ name, coinConstructor }) =>
coinFactory.register(name, coinConstructor)
);

// Generic ERC20 token registration for coins with SUPPORTS_ERC20 feature
coins
.filter((coin) => coin.features.includes(CoinFeature.SUPPORTS_ERC20) && !coin.isToken)
.forEach((coin) => {
const coinNames = {
Mainnet: `${coin.name}`,
Testnet: `t${coin.name}`,
};

EthLikeErc20Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => {
coinFactory.register(name, coinConstructor);
});
});
}

export function getCoinConstructor(coinName: string): CoinConstructor | undefined {
Expand Down
4 changes: 2 additions & 2 deletions modules/bitgo/src/v2/coins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { Dot, Tdot } from '@bitgo/sdk-coin-dot';
import { Eos, EosToken, Teos } from '@bitgo/sdk-coin-eos';
import { Etc, Tetc } from '@bitgo/sdk-coin-etc';
import { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth';
import { EvmCoin } from '@bitgo/sdk-coin-evm';
import { EvmCoin, EthLikeErc20Token } from '@bitgo/sdk-coin-evm';
import { Flr, Tflr } from '@bitgo/sdk-coin-flr';
import { Ethw } from '@bitgo/sdk-coin-ethw';
import { EthLikeCoin, TethLikeCoin } from '@bitgo/sdk-coin-ethlike';
Expand Down Expand Up @@ -108,7 +108,7 @@ export { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth };
export { Ethw };
export { EthLikeCoin, TethLikeCoin };
export { Etc, Tetc };
export { EvmCoin };
export { EvmCoin, EthLikeErc20Token };
export { Flr, Tflr };
export { Hash, Thash };
export { Hbar, Thbar };
Expand Down
1 change: 1 addition & 0 deletions modules/bitgo/test/browser/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('Coins', () => {
CosmosToken: 1,
CosmosSharedCoin: 1,
VetToken: 1,
EthLikeErc20Token: 1,
};
Object.keys(BitGoJS.Coin)
.filter((coinName) => !excludedKeys[coinName])
Expand Down
55 changes: 55 additions & 0 deletions modules/sdk-coin-evm/src/ethLikeErc20Token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @prettier
*/
import { coins, EthLikeTokenConfig } from '@bitgo/statics';
import { BitGoBase, CoinConstructor, common, MPCAlgorithm, NamedCoinConstructor } from '@bitgo/sdk-core';
import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth';
import { TransactionBuilder } from './lib';
import assert from 'assert';

export class EthLikeErc20Token extends EthLikeToken {
public readonly tokenConfig: EthLikeTokenConfig;
private readonly coinNames: CoinNames;

constructor(bitgo: BitGoBase, tokenConfig: EthLikeTokenConfig, coinNames: CoinNames) {
super(bitgo, tokenConfig, coinNames);
this.coinNames = coinNames;
}

static createTokenConstructor(config: EthLikeTokenConfig, coinNames: CoinNames): CoinConstructor {
return (bitgo: BitGoBase) => new this(bitgo, config, coinNames);
}

static createTokenConstructors(coinNames: CoinNames): NamedCoinConstructor[] {
return super.createTokenConstructors(coinNames);
}

protected getTransactionBuilder(): TransactionBuilder {
return new TransactionBuilder(coins.get(this.getBaseChain()));
}

getMPCAlgorithm(): MPCAlgorithm {
return 'ecdsa';
}

supportsTss(): boolean {
return true;
}

async recoveryBlockchainExplorerQuery(query: Record<string, string>): Promise<Record<string, unknown>> {
const family = this.getFamily();
const evmConfig = common.Environments[this.bitgo.getEnv()].evm;
assert(
evmConfig && this.getFamily() in evmConfig,
`env config is missing for ${this.getFamily()} in ${this.bitgo.getEnv()}`
);
const explorerUrl = evmConfig[family].baseUrl;
const apiToken = evmConfig[family].apiToken;
return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken);
}

//TODO: implement a way to return the coin family name or coin name instead of standard ERC20 Token.
getFullName(): string {
return 'ERC20 Token';
}
}
1 change: 1 addition & 0 deletions modules/sdk-coin-evm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './evmCoin';
export * from './lib';
export * from './register';
export * from './ethLikeErc20Token';
28 changes: 21 additions & 7 deletions modules/sdk-coin-evm/src/register.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BitGoBase } from '@bitgo/sdk-core';
import { CoinFeature, coins, NetworkType } from '@bitgo/statics';
import { EvmCoin } from './evmCoin';
import { EthLikeErc20Token } from './ethLikeErc20Token';

export const registerAll = (sdk: BitGoBase): void => {
coins
Expand All @@ -14,12 +15,25 @@ export const registerAll = (sdk: BitGoBase): void => {
};

export const register = (coinFamily: string, sdk: BitGoBase): void => {
if (coins.get(coinFamily).features.includes(CoinFeature.SHARED_EVM_SDK)) {
coins
.filter((coin) => coin.family === coinFamily && !coin.isToken)
.forEach((coin) => {
const coinFeatures = coins.get(coinFamily).features;
coins
.filter((coin) => coin.family === coinFamily && !coin.isToken)
.forEach((coin) => {
// Handle SHARED_EVM_SDK registration
if (coinFeatures.includes(CoinFeature.SHARED_EVM_SDK)) {
sdk.register(coin.name, EvmCoin.createInstance);
});
//TODO: add token registration after EVM Token Optimisation is implemented
}
}

// Handle SUPPORTS_ERC20 registration
if (coinFeatures.includes(CoinFeature.SUPPORTS_ERC20)) {
const coinNames = {
Mainnet: `${coin.name}`,
Testnet: `${coin.name}`,
};

EthLikeErc20Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => {
sdk.register(name, coinConstructor);
});
}
});
};
54 changes: 54 additions & 0 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,12 @@ export class AdaCoin extends AccountCoinToken {
}
}

export class EthLikeERC20Token extends ContractAddressDefinedToken {
constructor(options: Erc20ConstructorOptions) {
super(options);
}
}

/**
* The AVAX C Chain network support tokens
* AVAX C Chain Tokens are ERC20 coins
Expand Down Expand Up @@ -797,6 +803,54 @@ export function gasTankAccount(
);
}

/**
* Factory function for ethLikeErc20 token instances.
*
* @param id uuid v4
* @param name unique identifier of the token
* @param fullName Complete human-readable name of the token
* @param decimalPlaces Number of decimal places this token supports
* @param contractAddress Contract address of this token
* @param asset Asset which this coin represents
* @param network Optional token network
* @param coinNames The parent coin names for mainnet and testnet
* @param features Features of this coin
* @param prefix Optional token prefix
* @param suffix Optional token suffix
* @param primaryKeyCurve The elliptic curve for this chain/token
*/
export function erc20Token(
id: string,
name: string,
fullName: string,
decimalPlaces: number,
contractAddress: string,
asset: UnderlyingAsset,
network: AccountNetwork,
features: CoinFeature[] = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559],
prefix = '',
suffix: string = name.toUpperCase(),
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1
) {
return Object.freeze(
new EthLikeERC20Token({
id,
name,
fullName,
network,
contractAddress,
decimalPlaces,
asset,
features,
prefix,
suffix,
primaryKeyCurve,
isToken: true,
baseUnit: BaseUnit.ETH,
})
);
}

/**
* Factory function for erc20 token instances.
*
Expand Down
5 changes: 5 additions & 0 deletions modules/statics/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ export enum CoinFeature {
*/
SHARED_EVM_SDK = 'shared-evm-sdk',

/**
* This coin supports erc20 tokens
*/
SUPPORTS_ERC20 = 'supports-erc20-token',

/**
* This coin is a Cosmos coin and should use shared Cosmos SDK module
*/
Expand Down
Loading