From 04cbec35c67a4218830b79ba17778849474aef9e Mon Sep 17 00:00:00 2001 From: Prabhsharan Singh Date: Thu, 13 Aug 2026 01:10:36 +0530 Subject: [PATCH] feat(statics): add ERC-7984 wrapper metadata for Hoodi test pairs Carry underlyingErc20Address, rate, and requiresApprovalReset on Erc7984Coin for Hoodi terc7984 tokens only, leaving mainnet served configs unchanged. Ticket: CHALO-1156 Co-authored-by: Cursor --- modules/statics/src/account.ts | 43 +++++++++++++++++++-- modules/statics/src/coins/erc7984Tokens.ts | 14 +++++-- modules/statics/src/tokenConfig.ts | 14 ++++++- modules/statics/test/unit/erc7984Tokens.ts | 44 ++++++++++++++++++++++ 4 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 modules/statics/test/unit/erc7984Tokens.ts diff --git a/modules/statics/src/account.ts b/modules/statics/src/account.ts index 5cbc9929d3..322cc66033 100644 --- a/modules/statics/src/account.ts +++ b/modules/statics/src/account.ts @@ -203,6 +203,12 @@ export interface Erc721ConstructorOptions extends AccountConstructorOptions { export interface Erc7984ConstructorOptions extends AccountConstructorOptions { contractAddress: string; + /** Plain ERC-20 locked in the wrapper as escrow. Set for Hoodi test pairs first. */ + underlyingErc20Address?: string; + /** On-chain `rate()` as a decimal string (uint256). Set for Hoodi test pairs first. */ + rate?: string; + /** When true, non-zero→non-zero approve may revert (USDT-style); WP must approve(0) first. */ + requiresApprovalReset?: boolean; } export interface NFTCollectionIdConstructorOptions extends AccountConstructorOptions { @@ -415,11 +421,23 @@ export class Erc1155Coin extends ContractAddressDefinedToken {} * Token balances are stored as FHE-encrypted ciphertexts; transfers use confidentialTransfer() * instead of the standard ERC-20 transfer(). Balance reads require delegated decryption via ACL. * + * Wrapper metadata (`underlyingErc20Address`, `rate`, `requiresApprovalReset`) lives on the coin — + * same pattern as `Nep141Token.storageDepositAmount` / `XrpMptCoin.canTransfer`. Currently + * populated for Hoodi test pairs only so mainnet served configs stay unchanged. Allowlisting / + * vetting for shield/unshield is owned by AMS, not statics. + * * {@link https://eips.ethereum.org/EIPS/eip-7984 EIP-7984} */ export class Erc7984Coin extends ContractAddressDefinedToken { + public underlyingErc20Address?: string; + public rate?: string; + public requiresApprovalReset?: boolean; + constructor(options: Erc7984ConstructorOptions) { super(options); + this.underlyingErc20Address = options.underlyingErc20Address?.toLowerCase(); + this.rate = options.rate; + this.requiresApprovalReset = options.requiresApprovalReset; } } @@ -1266,6 +1284,9 @@ export function terc20( * @param suffix? Optional token suffix. Defaults to token name. * @param network? Optional token network. Defaults to Ethereum main network. * @param primaryKeyCurve The elliptic curve for this chain/token + * @param underlyingErc20Address? Plain ERC-20 locked in the wrapper as escrow + * @param rate? On-chain `rate()` as a decimal string (uint256) + * @param requiresApprovalReset? USDT-style approve(0) reset required before approve(amount) */ export function erc7984( id: string, @@ -1278,7 +1299,10 @@ export function erc7984( prefix = '', suffix: string = name.toUpperCase(), network: EthereumNetwork = Networks.main.ethereum, - primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1 + primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1, + underlyingErc20Address?: string, + rate?: string, + requiresApprovalReset?: boolean ) { return Object.freeze( new Erc7984Coin({ @@ -1287,6 +1311,9 @@ export function erc7984( fullName, network, contractAddress, + underlyingErc20Address, + rate, + requiresApprovalReset, prefix, suffix, features, @@ -1301,12 +1328,16 @@ export function erc7984( /** * Factory function for testnet ERC-7984 confidential token instances (Zama fhEVM). + * Requires wrapper↔underlying metadata used by shield/unshield flows. * * @param id uuid v4 * @param name unique identifier of the token (e.g. 'hteth:ctkn') * @param fullName Complete human-readable name of the token * @param decimalPlaces Number of decimal places this token supports (divisibility exponent) - * @param contractAddress Contract address of this token + * @param contractAddress Contract address of this token (wrapper) + * @param underlyingErc20Address Plain ERC-20 locked in the wrapper as escrow + * @param rate On-chain `rate()` as a decimal string (uint256) + * @param requiresApprovalReset USDT-style approve(0) reset required before approve(amount) * @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin. * @param features? Features of this coin. Defaults to ERC7984_TOKEN_FEATURES * @param prefix? Optional token prefix. Defaults to empty string @@ -1320,6 +1351,9 @@ export function terc7984( fullName: string, decimalPlaces: number, contractAddress: string, + underlyingErc20Address: string, + rate: string, + requiresApprovalReset: boolean, asset: UnderlyingAsset, features: CoinFeature[] = ERC7984_TOKEN_FEATURES, prefix = '', @@ -1338,7 +1372,10 @@ export function terc7984( prefix, suffix, network, - primaryKeyCurve + primaryKeyCurve, + underlyingErc20Address, + rate, + requiresApprovalReset ); } diff --git a/modules/statics/src/coins/erc7984Tokens.ts b/modules/statics/src/coins/erc7984Tokens.ts index 3862419496..9b393ad017 100644 --- a/modules/statics/src/coins/erc7984Tokens.ts +++ b/modules/statics/src/coins/erc7984Tokens.ts @@ -8,13 +8,13 @@ import { UnderlyingAsset } from '../base'; * Balances are stored as encrypted ciphertexts; plaintext amounts require ACL-delegated * decryption via the Zama Gateway before they can be displayed. * + * Wrapper↔underlying metadata (`underlyingErc20Address`, `rate`, `requiresApprovalReset`) + * is wired on Hoodi test pairs only for now so mainnet served configs stay unchanged. + * Allowlisting is owned by AMS. + * * Testnet tokens (hteth:*) are deployed on Hoodi using Zama's cleartext FHE stack. * Hoodi ACL: 0x6D3FAf6f86e1fF9F3B0831Dda920AbA1cBd5bd68 (Networks.test.hoodi.zamaAclContractAddress) * Hoodi RPC: https://rpc.hoodi.ethpandaops.io (chain ID 560048) - * - * Sandbox development contracts (Sepolia, real FHE stack): - * CTKN: 0x94167129172A35ab093B44b8b96213DDbc3cD387 - * cUSDT: 0x4E7B06D78965594eB5EF5414c357ca21E1554491 */ export const erc7984Tokens = [ // Mainnet tokens @@ -74,6 +74,9 @@ export const erc7984Tokens = [ 'Zama Token Test 1', 6, '0x7b1d59bbcd291daa59cb6c8c5bc04de1afc4aba1', + '0x7740f913dc24d4f9e1a72531372c3170452b2f87', + '1000000000000', // 1e12 + false, UnderlyingAsset['hteth:ctest1'] ), terc7984( @@ -82,6 +85,9 @@ export const erc7984Tokens = [ 'Zama USDT', 6, '0x2debbe0487ef921df4457f9e36ed05be2df1ac75', + '0x51a63b5621d78de54d2f4d098a23a5a69e76f30b', + '1', + false, UnderlyingAsset['hteth:cusdt'] ), ]; diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index e49ad7eeb3..6362b8b029 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -70,7 +70,12 @@ export type EosTokenConfig = BaseContractAddressConfig & { contractAddress: string; }; export type Erc20TokenConfig = BaseContractAddressConfig; -export type Erc7984TokenConfig = BaseContractAddressConfig; +export type Erc7984TokenConfig = BaseContractAddressConfig & { + /** Present for Hoodi test pairs; omitted on mainnet until shield/unshield ships there. */ + underlyingErc20Address?: string; + rate?: string; + requiresApprovalReset?: boolean; +}; export type TrxTokenConfig = BaseContractAddressConfig; export type StellarTokenConfig = BaseNetworkConfig; @@ -386,6 +391,13 @@ function getErc7984TokenConfig(coin: Erc7984Coin): Erc7984TokenConfig { name: coin.fullName, tokenContractAddress: coin.contractAddress.toString().toLowerCase(), decimalPlaces: coin.decimalPlaces, + ...(coin.underlyingErc20Address !== undefined && { + underlyingErc20Address: coin.underlyingErc20Address.toLowerCase(), + }), + ...(coin.rate !== undefined && { rate: coin.rate }), + ...(coin.requiresApprovalReset !== undefined && { + requiresApprovalReset: coin.requiresApprovalReset, + }), }; } diff --git a/modules/statics/test/unit/erc7984Tokens.ts b/modules/statics/test/unit/erc7984Tokens.ts new file mode 100644 index 0000000000..5b9ba773d0 --- /dev/null +++ b/modules/statics/test/unit/erc7984Tokens.ts @@ -0,0 +1,44 @@ +import 'should'; +import { coins, Erc7984Coin, Networks } from '../../src'; + +describe('ERC-7984 wrapper metadata on Erc7984Coin', function () { + function getErc7984(name: string): Erc7984Coin { + const coin = coins.get(name); + should.exist(coin); + (coin instanceof Erc7984Coin).should.equal(true); + return coin as Erc7984Coin; + } + + it('includes a Hoodi test pair', function () { + const hoodi = getErc7984('hteth:cusdt'); + hoodi.network.name.should.equal(Networks.test.hoodi.name); + }); + + it('stores on-chain underlying and rate for Hoodi cUSDT', function () { + const coin = getErc7984('hteth:cusdt'); + coin.contractAddress.should.equal('0x2debbe0487ef921df4457f9e36ed05be2df1ac75'); + coin.underlyingErc20Address.should.equal('0x51a63b5621d78de54d2f4d098a23a5a69e76f30b'); + coin.rate.should.equal('1'); + coin.requiresApprovalReset.should.equal(false); + }); + + it('stores scaled rate for Hoodi cTEST1', function () { + const coin = getErc7984('hteth:ctest1'); + coin.underlyingErc20Address.should.equal('0x7740f913dc24d4f9e1a72531372c3170452b2f87'); + coin.rate.should.equal('1000000000000'); + coin.requiresApprovalReset.should.equal(false); + }); + + it('leaves mainnet wrapper metadata unset', function () { + const coin = getErc7984('eth:cusdt'); + should.equal(coin.underlyingErc20Address, undefined); + should.equal(coin.rate, undefined); + should.equal(coin.requiresApprovalReset, undefined); + }); + + it('lowercases addresses on construction for test pairs', function () { + const coin = getErc7984('hteth:cusdt'); + coin.contractAddress.should.equal(coin.contractAddress.toLowerCase()); + coin.underlyingErc20Address!.should.equal(coin.underlyingErc20Address!.toLowerCase()); + }); +});