Skip to content

Commit 8a2f38f

Browse files
0xPrabhcursoragent
andcommitted
feat(statics): add ERC-7984 wrapper↔underlying pair metadata
Carry wrapper pair metadata on Erc7984Coin and the served Erc7984TokenConfig: underlyingErc20Address, rate, requiresApprovalReset, and isVetted. Follows the same pattern as Nep141Token.storageDepositAmount and XrpMptCoin.canTransfer rather than a separate registry, so shield/unshield consumers read it straight off the coin. Values come from on-chain rate() / underlying() view calls; Hoodi test pairs are wired alongside the existing mainnet tokens. Ticket: CHALO-1156 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b5e50dd commit 8a2f38f

4 files changed

Lines changed: 130 additions & 10 deletions

File tree

modules/statics/src/account.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ export interface Erc721ConstructorOptions extends AccountConstructorOptions {
203203

204204
export interface Erc7984ConstructorOptions extends AccountConstructorOptions {
205205
contractAddress: string;
206+
/** Plain ERC-20 locked in the wrapper as escrow. */
207+
underlyingErc20Address: string;
208+
/** On-chain `rate()` as a decimal string (uint256). */
209+
rate: string;
210+
/** When true, non-zero→non-zero approve may revert (USDT-style); WP must approve(0) first. */
211+
requiresApprovalReset: boolean;
212+
/** When true, pair is approved for shield/unshield + indexer publish filters. */
213+
isVetted: boolean;
206214
}
207215

208216
export interface NFTCollectionIdConstructorOptions extends AccountConstructorOptions {
@@ -415,11 +423,23 @@ export class Erc1155Coin extends ContractAddressDefinedToken {}
415423
* Token balances are stored as FHE-encrypted ciphertexts; transfers use confidentialTransfer()
416424
* instead of the standard ERC-20 transfer(). Balance reads require delegated decryption via ACL.
417425
*
426+
* Wrapper metadata (`underlyingErc20Address`, `rate`, `requiresApprovalReset`, `isVetted`) lives
427+
* on the coin — same pattern as `Nep141Token.storageDepositAmount` / `XrpMptCoin.canTransfer`.
428+
*
418429
* {@link https://eips.ethereum.org/EIPS/eip-7984 EIP-7984}
419430
*/
420431
export class Erc7984Coin extends ContractAddressDefinedToken {
432+
public underlyingErc20Address: string;
433+
public rate: string;
434+
public requiresApprovalReset: boolean;
435+
public isVetted: boolean;
436+
421437
constructor(options: Erc7984ConstructorOptions) {
422438
super(options);
439+
this.underlyingErc20Address = options.underlyingErc20Address.toLowerCase();
440+
this.rate = options.rate;
441+
this.requiresApprovalReset = options.requiresApprovalReset;
442+
this.isVetted = options.isVetted;
423443
}
424444
}
425445

@@ -1259,26 +1279,34 @@ export function terc20(
12591279
* @param name unique identifier of the token (e.g. 'eth:ctkn')
12601280
* @param fullName Complete human-readable name of the token
12611281
* @param decimalPlaces Number of decimal places this token supports (divisibility exponent)
1262-
* @param contractAddress Contract address of this token
1282+
* @param contractAddress Contract address of this token (wrapper)
1283+
* @param underlyingErc20Address Plain ERC-20 locked in the wrapper as escrow
1284+
* @param rate On-chain `rate()` as a decimal string (uint256)
1285+
* @param requiresApprovalReset USDT-style approve(0) reset required before approve(amount)
12631286
* @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin.
12641287
* @param features? Features of this coin. Defaults to ERC7984_TOKEN_FEATURES
12651288
* @param prefix? Optional token prefix. Defaults to empty string
12661289
* @param suffix? Optional token suffix. Defaults to token name.
12671290
* @param network? Optional token network. Defaults to Ethereum main network.
12681291
* @param primaryKeyCurve The elliptic curve for this chain/token
1292+
* @param isVetted? When true, approved for shield/unshield + indexer publish. Defaults to true.
12691293
*/
12701294
export function erc7984(
12711295
id: string,
12721296
name: string,
12731297
fullName: string,
12741298
decimalPlaces: number,
12751299
contractAddress: string,
1300+
underlyingErc20Address: string,
1301+
rate: string,
1302+
requiresApprovalReset: boolean,
12761303
asset: UnderlyingAsset,
12771304
features: CoinFeature[] = ERC7984_TOKEN_FEATURES,
12781305
prefix = '',
12791306
suffix: string = name.toUpperCase(),
12801307
network: EthereumNetwork = Networks.main.ethereum,
1281-
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1
1308+
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1,
1309+
isVetted = true
12821310
) {
12831311
return Object.freeze(
12841312
new Erc7984Coin({
@@ -1287,6 +1315,10 @@ export function erc7984(
12871315
fullName,
12881316
network,
12891317
contractAddress,
1318+
underlyingErc20Address,
1319+
rate,
1320+
requiresApprovalReset,
1321+
isVetted,
12901322
prefix,
12911323
suffix,
12921324
features,
@@ -1306,39 +1338,51 @@ export function erc7984(
13061338
* @param name unique identifier of the token (e.g. 'hteth:ctkn')
13071339
* @param fullName Complete human-readable name of the token
13081340
* @param decimalPlaces Number of decimal places this token supports (divisibility exponent)
1309-
* @param contractAddress Contract address of this token
1341+
* @param contractAddress Contract address of this token (wrapper)
1342+
* @param underlyingErc20Address Plain ERC-20 locked in the wrapper as escrow
1343+
* @param rate On-chain `rate()` as a decimal string (uint256)
1344+
* @param requiresApprovalReset USDT-style approve(0) reset required before approve(amount)
13101345
* @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin.
13111346
* @param features? Features of this coin. Defaults to ERC7984_TOKEN_FEATURES
13121347
* @param prefix? Optional token prefix. Defaults to empty string
13131348
* @param suffix? Optional token suffix. Defaults to token name.
13141349
* @param network? Optional token network. Defaults to Hoodi test network.
13151350
* @param primaryKeyCurve The elliptic curve for this chain/token
1351+
* @param isVetted? When true, approved for shield/unshield + indexer publish. Defaults to true.
13161352
*/
13171353
export function terc7984(
13181354
id: string,
13191355
name: string,
13201356
fullName: string,
13211357
decimalPlaces: number,
13221358
contractAddress: string,
1359+
underlyingErc20Address: string,
1360+
rate: string,
1361+
requiresApprovalReset: boolean,
13231362
asset: UnderlyingAsset,
13241363
features: CoinFeature[] = ERC7984_TOKEN_FEATURES,
13251364
prefix = '',
13261365
suffix: string = name.toUpperCase(),
13271366
network: EthereumNetwork = Networks.test.hoodi,
1328-
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1
1367+
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1,
1368+
isVetted = true
13291369
) {
13301370
return erc7984(
13311371
id,
13321372
name,
13331373
fullName,
13341374
decimalPlaces,
13351375
contractAddress,
1376+
underlyingErc20Address,
1377+
rate,
1378+
requiresApprovalReset,
13361379
asset,
13371380
features,
13381381
prefix,
13391382
suffix,
13401383
network,
1341-
primaryKeyCurve
1384+
primaryKeyCurve,
1385+
isVetted
13421386
);
13431387
}
13441388

modules/statics/src/coins/erc7984Tokens.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import { UnderlyingAsset } from '../base';
88
* Balances are stored as encrypted ciphertexts; plaintext amounts require ACL-delegated
99
* decryption via the Zama Gateway before they can be displayed.
1010
*
11+
* Each entry is a wrapper↔underlying pair. `rate`, `requiresApprovalReset`, and `isVetted`
12+
* are taken from on-chain `rate()` / known approve semantics / vetting status.
13+
*
1114
* Testnet tokens (hteth:*) are deployed on Hoodi using Zama's cleartext FHE stack.
1215
* Hoodi ACL: 0x6D3FAf6f86e1fF9F3B0831Dda920AbA1cBd5bd68 (Networks.test.hoodi.zamaAclContractAddress)
1316
* Hoodi RPC: https://rpc.hoodi.ethpandaops.io (chain ID 560048)
14-
*
15-
* Sandbox development contracts (Sepolia, real FHE stack):
16-
* CTKN: 0x94167129172A35ab093B44b8b96213DDbc3cD387
17-
* cUSDT: 0x4E7B06D78965594eB5EF5414c357ca21E1554491
1817
*/
1918
export const erc7984Tokens = [
2019
// Mainnet tokens
@@ -24,6 +23,9 @@ export const erc7984Tokens = [
2423
'Confidential Zama',
2524
6,
2625
'0x80cb147fd86dc6dee3eee7e4cee33d1397d98071', // https://etherscan.io/token/0x80CB147Fd86dC6dEe3Eee7e4Cee33d1397d98071
26+
'0xa12cc123ba206d4031d1c7f6223d1c2ec249f4f3',
27+
'1000000000000', // 1e12
28+
false,
2729
UnderlyingAsset['eth:czama']
2830
),
2931
erc7984(
@@ -32,6 +34,9 @@ export const erc7984Tokens = [
3234
'Zama Confidential XAUT',
3335
6,
3436
'0x73cc9af9d6befdb3c3faf8a5e8c05cb95fdaeef1', // https://etherscan.io/token/0x73cc9aF9d6BEFdb3c3fAf8a5E8c05Cb95FdaEEf1
37+
'0x68749665ff8d2d112fa859aa293f07a622782f38',
38+
'1',
39+
false,
3540
UnderlyingAsset['eth:cxaut']
3641
),
3742
erc7984(
@@ -40,6 +45,9 @@ export const erc7984Tokens = [
4045
'Zama Confidential TGBP',
4146
6,
4247
'0xa873750ccbafd5ec7dd13bfd5237d7129832edd9', // https://etherscan.io/token/0xa873750ccBafD5ec7Dd13bfD5237d7129832eDD9
48+
'0x27f6c8289550fce67f6b50bed1f519966afe5287',
49+
'1000000000000', // 1e12
50+
false,
4351
UnderlyingAsset['eth:ctgbp']
4452
),
4553
erc7984(
@@ -48,6 +56,9 @@ export const erc7984Tokens = [
4856
'Zama Confidential WETH',
4957
6,
5058
'0xda9396b82634ea99243ce51258b6a5ae512d4893', // https://etherscan.io/token/0xda9396b82634Ea99243cE51258B6A5Ae512D4893
59+
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH
60+
'1000000000000', // 1e12
61+
false,
5162
UnderlyingAsset['eth:cweth']
5263
),
5364
erc7984(
@@ -56,6 +67,9 @@ export const erc7984Tokens = [
5667
'Zama USDT',
5768
6,
5869
'0xae0207c757aa2b4019ad96edd0092ddc63ef0c50', // https://etherscan.io/token/0xAe0207C757Aa2B4019Ad96edD0092ddc63EF0c50
70+
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
71+
'1',
72+
true,
5973
UnderlyingAsset['eth:cusdt']
6074
),
6175
erc7984(
@@ -64,6 +78,9 @@ export const erc7984Tokens = [
6478
'Zama USDC',
6579
6,
6680
'0xe978f22157048e5db8e5d07971376e86671672b2', // https://etherscan.io/token/0xe978f22157048e5db8e5d07971376e86671672b2
81+
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
82+
'1',
83+
false,
6784
UnderlyingAsset['eth:cusdc']
6885
),
6986

@@ -74,6 +91,9 @@ export const erc7984Tokens = [
7491
'Zama Token Test 1',
7592
6,
7693
'0x7b1d59bbcd291daa59cb6c8c5bc04de1afc4aba1',
94+
'0x7740f913dc24d4f9e1a72531372c3170452b2f87',
95+
'1000000000000', // 1e12
96+
false,
7797
UnderlyingAsset['hteth:ctest1']
7898
),
7999
terc7984(
@@ -82,6 +102,9 @@ export const erc7984Tokens = [
82102
'Zama USDT',
83103
6,
84104
'0x2debbe0487ef921df4457f9e36ed05be2df1ac75',
105+
'0x51a63b5621d78de54d2f4d098a23a5a69e76f30b',
106+
'1',
107+
false,
85108
UnderlyingAsset['hteth:cusdt']
86109
),
87110
];

modules/statics/src/tokenConfig.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,12 @@ export type EosTokenConfig = BaseContractAddressConfig & {
7070
contractAddress: string;
7171
};
7272
export type Erc20TokenConfig = BaseContractAddressConfig;
73-
export type Erc7984TokenConfig = BaseContractAddressConfig;
73+
export type Erc7984TokenConfig = BaseContractAddressConfig & {
74+
underlyingErc20Address: string;
75+
rate: string;
76+
requiresApprovalReset: boolean;
77+
isVetted: boolean;
78+
};
7479
export type TrxTokenConfig = BaseContractAddressConfig;
7580
export type StellarTokenConfig = BaseNetworkConfig;
7681

@@ -386,6 +391,10 @@ function getErc7984TokenConfig(coin: Erc7984Coin): Erc7984TokenConfig {
386391
name: coin.fullName,
387392
tokenContractAddress: coin.contractAddress.toString().toLowerCase(),
388393
decimalPlaces: coin.decimalPlaces,
394+
underlyingErc20Address: coin.underlyingErc20Address.toLowerCase(),
395+
rate: coin.rate,
396+
requiresApprovalReset: coin.requiresApprovalReset,
397+
isVetted: coin.isVetted,
389398
};
390399
}
391400

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'should';
2+
import { coins, Erc7984Coin, Networks } from '../../src';
3+
4+
describe('ERC-7984 wrapper metadata on Erc7984Coin', function () {
5+
function getErc7984(name: string): Erc7984Coin {
6+
const coin = coins.get(name);
7+
should.exist(coin);
8+
(coin instanceof Erc7984Coin).should.equal(true);
9+
return coin as Erc7984Coin;
10+
}
11+
12+
it('includes a vetted Hoodi test pair', function () {
13+
const hoodi = getErc7984('hteth:cusdt');
14+
hoodi.network.name.should.equal(Networks.test.hoodi.name);
15+
hoodi.isVetted.should.equal(true);
16+
});
17+
18+
it('stores on-chain underlying and rate for Hoodi cUSDT', function () {
19+
const coin = getErc7984('hteth:cusdt');
20+
coin.contractAddress.should.equal('0x2debbe0487ef921df4457f9e36ed05be2df1ac75');
21+
coin.underlyingErc20Address.should.equal('0x51a63b5621d78de54d2f4d098a23a5a69e76f30b');
22+
coin.rate.should.equal('1');
23+
coin.requiresApprovalReset.should.equal(false);
24+
});
25+
26+
it('marks mainnet USDT wrapper as requiring approval reset', function () {
27+
const coin = getErc7984('eth:cusdt');
28+
coin.underlyingErc20Address.should.equal('0xdac17f958d2ee523a2206206994597c13d831ec7');
29+
coin.requiresApprovalReset.should.equal(true);
30+
coin.rate.should.equal('1');
31+
});
32+
33+
it('exposes non-1 rates for wrappers that scale decimals', function () {
34+
const coin = getErc7984('eth:cweth');
35+
coin.rate.should.equal('1000000000000');
36+
coin.underlyingErc20Address.should.equal('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2');
37+
});
38+
39+
it('lowercases addresses on construction', function () {
40+
const coin = getErc7984('eth:cusdc');
41+
coin.contractAddress.should.equal(coin.contractAddress.toLowerCase());
42+
coin.underlyingErc20Address.should.equal(coin.underlyingErc20Address.toLowerCase());
43+
});
44+
});

0 commit comments

Comments
 (0)