From 48beda4d36c03bf47fda2fa3ee24f35caf0cbdbb Mon Sep 17 00:00:00 2001 From: BitGo Agent Date: Tue, 21 Jul 2026 13:20:41 +0000 Subject: [PATCH] feat(statics): add TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY CoinFeatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new values to the CoinFeature enum in @bitgo/statics: - TOKENIZED_EQUITY: economic-nature marker for any tokenized stock. Drives generic behavior (pricing off the underlying, asset-class reporting). May be set for BitGo-issued or third-party tokens. - BITGO_TOKENIZED_EQUITY: BitGo first-party permissioned goStocks marker. Drives whitelist-destination withdrawal validation and allowlist/segregation behavior. Requires TOKENIZED_EQUITY to also be set (invariant enforced in BaseCoin.validateOptions). Apply both flags to all goSPCX representations: sol:gospcx, tsol:gospcx, tsol:stggospcx (on-chain tokens) and ofcsol:gospcx, ofctsol:gospcx, ofctsol:stggospcx (OFC mirrors). No other goStock tokens receive these flags in this ticket; behavioral enforcement (withdrawal allowlist checks) is out of scope and tracked in TDD ยง13.4. Ticket: SCAAS-10372 Session-Id: a4128cc9-058b-4a15-ab67-160780260e8a Task-Id: a0f7361a-becc-4c21-ab18-99920c0c7a35 --- modules/statics/src/base.ts | 20 ++++++ modules/statics/src/coins/ofcCoins.ts | 6 +- modules/statics/src/coins/solTokens.ts | 6 +- modules/statics/test/unit/base.ts | 88 +++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 7 deletions(-) diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 42f7cc74e3..7739756262 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -602,6 +602,20 @@ export enum CoinFeature { * This coin supports bring-your-own-validator (BYOV) staking */ BYOV_VALIDATOR = 'byov-validator', + + /** + * This coin is a tokenized equity (e.g. a tokenized stock). Drives generic + * behavior such as pricing off the underlying and asset-class reporting. + * May be set for both BitGo-issued and third-party tokenized equities. + */ + TOKENIZED_EQUITY = 'tokenized-equity', + + /** + * This coin is a BitGo first-party permissioned tokenized equity (a goStock). + * Drives whitelist-destination withdrawal validation and allowlist/segregation + * behavior. MUST only be set together with TOKENIZED_EQUITY. + */ + BITGO_TOKENIZED_EQUITY = 'bitgo-tokenized-equity', } /** @@ -4623,6 +4637,12 @@ export abstract class BaseCoin { throw new MissingRequiredCoinFeatureError(options.name, Array.from(requiredFeatures)); } + // BITGO_TOKENIZED_EQUITY requires TOKENIZED_EQUITY to also be present + const featureSet = new Set(options.features); + if (featureSet.has(CoinFeature.BITGO_TOKENIZED_EQUITY) && !featureSet.has(CoinFeature.TOKENIZED_EQUITY)) { + throw new MissingRequiredCoinFeatureError(options.name, [CoinFeature.TOKENIZED_EQUITY]); + } + // assets require a valid uuid v4 id if (!BaseCoin.isValidUuidV4(options.id)) { throw new InvalidIdError(options.name, options.id); diff --git a/modules/statics/src/coins/ofcCoins.ts b/modules/statics/src/coins/ofcCoins.ts index f78db3a577..a7779c07c1 100644 --- a/modules/statics/src/coins/ofcCoins.ts +++ b/modules/statics/src/coins/ofcCoins.ts @@ -1644,7 +1644,7 @@ export const ofcCoins = [ 'SPCX goStock', 6, UnderlyingAsset['sol:gospcx'], - [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN] + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, CoinFeature.TOKENIZED_EQUITY, CoinFeature.BITGO_TOKENIZED_EQUITY] ), ofcsolToken( 'e343b3c2-dcbb-4a9f-a60e-3dd79825c5fb', @@ -2034,7 +2034,7 @@ export const ofcCoins = [ 'Test SPCX goStock', 6, UnderlyingAsset['tsol:stggospcx'], - [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN] + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, CoinFeature.TOKENIZED_EQUITY, CoinFeature.BITGO_TOKENIZED_EQUITY] ), tofcsolToken( '84f901ed-5654-47a7-9d08-7ff7c27a8c16', @@ -2042,7 +2042,7 @@ export const ofcCoins = [ 'Test SPCX goStock', 6, UnderlyingAsset['tsol:gospcx'], - [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN] + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, CoinFeature.TOKENIZED_EQUITY, CoinFeature.BITGO_TOKENIZED_EQUITY] ), tofcsolToken( '750f0e40-c5b9-464f-874f-dc455cf1494b', diff --git a/modules/statics/src/coins/solTokens.ts b/modules/statics/src/coins/solTokens.ts index 3444082cac..f128ec0923 100644 --- a/modules/statics/src/coins/solTokens.ts +++ b/modules/statics/src/coins/solTokens.ts @@ -3686,7 +3686,7 @@ export const solTokens = [ '98iaHRfvCnaihEHcLK5EpwLq3w5yHiKKfSgpYLcxEScB', '98iaHRfvCnaihEHcLK5EpwLq3w5yHiKKfSgpYLcxEScB', UnderlyingAsset['tsol:stggospcx'], - [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN], + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, CoinFeature.TOKENIZED_EQUITY, CoinFeature.BITGO_TOKENIZED_EQUITY], ProgramID.Token2022ProgramId ), tsolToken( @@ -3697,7 +3697,7 @@ export const solTokens = [ '4gvEw3Lx2gkAByv4X8hLnHujneN5bwtMGSu2ZDoip4vj', '4gvEw3Lx2gkAByv4X8hLnHujneN5bwtMGSu2ZDoip4vj', UnderlyingAsset['tsol:gospcx'], - [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN], + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, CoinFeature.TOKENIZED_EQUITY, CoinFeature.BITGO_TOKENIZED_EQUITY], ProgramID.Token2022ProgramId ), solToken( @@ -3708,7 +3708,7 @@ export const solTokens = [ 'AAVvaNDwkGfxGNaf1HJ5JzfwDb1PYmAgXSixRsczyrk4', 'AAVvaNDwkGfxGNaf1HJ5JzfwDb1PYmAgXSixRsczyrk4', UnderlyingAsset['sol:gospcx'], - [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN], + [...SOL_TOKEN_FEATURES, CoinFeature.STABLECOIN, CoinFeature.TOKENIZED_EQUITY, CoinFeature.BITGO_TOKENIZED_EQUITY], ProgramID.Token2022ProgramId ), tsolToken( diff --git a/modules/statics/test/unit/base.ts b/modules/statics/test/unit/base.ts index 9a351cba45..191f9cdf67 100644 --- a/modules/statics/test/unit/base.ts +++ b/modules/statics/test/unit/base.ts @@ -1,7 +1,8 @@ -import { CoinFamily } from '../../src'; +import { CoinFamily, CoinFeature, coins } from '../../src'; const should = require('should'); const { UnderlyingAsset } = require('../../src/base'); +const { solToken, ProgramID } = require('../../src/account'); describe('UnderlyingAsset', function () { it('UnderlyingAsset values should be unique', function () { @@ -36,3 +37,88 @@ describe('zkSync Era Base Types', function () { UnderlyingAsset.ZKSYNCERA.should.equal('zksyncera'); }); }); + +describe('Tokenized Equity CoinFeatures', function () { + it('TOKENIZED_EQUITY feature value should be tokenized-equity', function () { + CoinFeature.TOKENIZED_EQUITY.should.equal('tokenized-equity'); + }); + + it('BITGO_TOKENIZED_EQUITY feature value should be bitgo-tokenized-equity', function () { + CoinFeature.BITGO_TOKENIZED_EQUITY.should.equal('bitgo-tokenized-equity'); + }); + + it('sol:gospcx should have both TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY', function () { + const coin = coins.get('sol:gospcx'); + coin.features.should.containEql(CoinFeature.TOKENIZED_EQUITY); + coin.features.should.containEql(CoinFeature.BITGO_TOKENIZED_EQUITY); + }); + + it('tsol:gospcx should have both TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY', function () { + const coin = coins.get('tsol:gospcx'); + coin.features.should.containEql(CoinFeature.TOKENIZED_EQUITY); + coin.features.should.containEql(CoinFeature.BITGO_TOKENIZED_EQUITY); + }); + + it('tsol:stggospcx should have both TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY', function () { + const coin = coins.get('tsol:stggospcx'); + coin.features.should.containEql(CoinFeature.TOKENIZED_EQUITY); + coin.features.should.containEql(CoinFeature.BITGO_TOKENIZED_EQUITY); + }); + + it('ofcsol:gospcx should have both TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY', function () { + const coin = coins.get('ofcsol:gospcx'); + coin.features.should.containEql(CoinFeature.TOKENIZED_EQUITY); + coin.features.should.containEql(CoinFeature.BITGO_TOKENIZED_EQUITY); + }); + + it('ofctsol:gospcx should have both TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY', function () { + const coin = coins.get('ofctsol:gospcx'); + coin.features.should.containEql(CoinFeature.TOKENIZED_EQUITY); + coin.features.should.containEql(CoinFeature.BITGO_TOKENIZED_EQUITY); + }); + + it('ofctsol:stggospcx should have both TOKENIZED_EQUITY and BITGO_TOKENIZED_EQUITY', function () { + const coin = coins.get('ofctsol:stggospcx'); + coin.features.should.containEql(CoinFeature.TOKENIZED_EQUITY); + coin.features.should.containEql(CoinFeature.BITGO_TOKENIZED_EQUITY); + }); + + it('invariant: BITGO_TOKENIZED_EQUITY without TOKENIZED_EQUITY should throw MissingRequiredCoinFeatureError', function () { + let threw = false; + let errorMessage = ''; + let errorType = ''; + try { + solToken( + '00000000-0000-0000-0000-000000000001', + 'test:invalidgostock', + 'Invalid goStock', + 6, + 'AAVvaNDwkGfxGNaf1HJ5JzfwDb1PYmAgXSixRsczyrk4', + 'AAVvaNDwkGfxGNaf1HJ5JzfwDb1PYmAgXSixRsczyrk4', + UnderlyingAsset['sol:gospcx'], + [ + CoinFeature.ACCOUNT_MODEL, + CoinFeature.REQUIRES_BIG_NUMBER, + CoinFeature.VALUELESS_TRANSFER, + CoinFeature.TRANSACTION_DATA, + CoinFeature.CUSTODY, + CoinFeature.CUSTODY_BITGO_TRUST, + CoinFeature.TSS, + CoinFeature.TSS_COLD, + CoinFeature.BULK_TRANSACTION, + CoinFeature.BITGO_TOKENIZED_EQUITY, + ], + ProgramID.Token2022ProgramId + ); + } catch (err: unknown) { + threw = true; + if (err instanceof Error) { + errorMessage = err.message; + errorType = err.constructor.name; + } + } + threw.should.be.true(); + errorType.should.equal('MissingRequiredCoinFeatureError'); + errorMessage.should.containEql('tokenized-equity'); + }); +});