Skip to content
Draft
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
54 changes: 49 additions & 5 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ export interface Erc721ConstructorOptions extends AccountConstructorOptions {

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

export interface NFTCollectionIdConstructorOptions extends AccountConstructorOptions {
Expand Down Expand Up @@ -415,11 +423,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`, `isVetted`) lives
* on the coin — same pattern as `Nep141Token.storageDepositAmount` / `XrpMptCoin.canTransfer`.
*
* {@link https://eips.ethereum.org/EIPS/eip-7984 EIP-7984}
*/
export class Erc7984Coin extends ContractAddressDefinedToken {
public underlyingErc20Address: string;
public rate: string;
public requiresApprovalReset: boolean;
public isVetted: boolean;

constructor(options: Erc7984ConstructorOptions) {
super(options);
this.underlyingErc20Address = options.underlyingErc20Address.toLowerCase();
this.rate = options.rate;
this.requiresApprovalReset = options.requiresApprovalReset;
this.isVetted = options.isVetted;
}
}

Expand Down Expand Up @@ -1259,26 +1279,34 @@ export function terc20(
* @param name unique identifier of the token (e.g. 'eth: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
* @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 isVetted? When true, approved for shield/unshield + indexer publish. Defaults to true.
*/
export function erc7984(
id: string,
name: string,
fullName: string,
decimalPlaces: number,
contractAddress: string,
underlyingErc20Address: string,
rate: string,
requiresApprovalReset: boolean,
asset: UnderlyingAsset,
features: CoinFeature[] = ERC7984_TOKEN_FEATURES,
prefix = '',
suffix: string = name.toUpperCase(),
network: EthereumNetwork = Networks.main.ethereum,
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1,
isVetted = true
) {
return Object.freeze(
new Erc7984Coin({
Expand All @@ -1287,6 +1315,10 @@ export function erc7984(
fullName,
network,
contractAddress,
underlyingErc20Address,
rate,
requiresApprovalReset,
isVetted,
prefix,
suffix,
features,
Expand All @@ -1306,39 +1338,51 @@ export function erc7984(
* @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
* @param suffix? Optional token suffix. Defaults to token name.
* @param network? Optional token network. Defaults to Hoodi test network.
* @param primaryKeyCurve The elliptic curve for this chain/token
* @param isVetted? When true, approved for shield/unshield + indexer publish. Defaults to true.
*/
export function terc7984(
id: string,
name: string,
fullName: string,
decimalPlaces: number,
contractAddress: string,
underlyingErc20Address: string,
rate: string,
requiresApprovalReset: boolean,
asset: UnderlyingAsset,
features: CoinFeature[] = ERC7984_TOKEN_FEATURES,
prefix = '',
suffix: string = name.toUpperCase(),
network: EthereumNetwork = Networks.test.hoodi,
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1,
isVetted = true
) {
return erc7984(
id,
name,
fullName,
decimalPlaces,
contractAddress,
underlyingErc20Address,
rate,
requiresApprovalReset,
asset,
features,
prefix,
suffix,
network,
primaryKeyCurve
primaryKeyCurve,
isVetted
);
}

Expand Down
31 changes: 27 additions & 4 deletions modules/statics/src/coins/erc7984Tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ 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.
*
* Each entry is a wrapper↔underlying pair. `rate`, `requiresApprovalReset`, and `isVetted`
* are taken from on-chain `rate()` / known approve semantics / vetting status.
*
* 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
Expand All @@ -24,6 +23,9 @@ export const erc7984Tokens = [
'Confidential Zama',
6,
'0x80cb147fd86dc6dee3eee7e4cee33d1397d98071', // https://etherscan.io/token/0x80CB147Fd86dC6dEe3Eee7e4Cee33d1397d98071
'0xa12cc123ba206d4031d1c7f6223d1c2ec249f4f3',
'1000000000000', // 1e12
false,
UnderlyingAsset['eth:czama']
),
erc7984(
Expand All @@ -32,6 +34,9 @@ export const erc7984Tokens = [
'Zama Confidential XAUT',
6,
'0x73cc9af9d6befdb3c3faf8a5e8c05cb95fdaeef1', // https://etherscan.io/token/0x73cc9aF9d6BEFdb3c3fAf8a5E8c05Cb95FdaEEf1
'0x68749665ff8d2d112fa859aa293f07a622782f38',
'1',
false,
UnderlyingAsset['eth:cxaut']
),
erc7984(
Expand All @@ -40,6 +45,9 @@ export const erc7984Tokens = [
'Zama Confidential TGBP',
6,
'0xa873750ccbafd5ec7dd13bfd5237d7129832edd9', // https://etherscan.io/token/0xa873750ccBafD5ec7Dd13bfD5237d7129832eDD9
'0x27f6c8289550fce67f6b50bed1f519966afe5287',
'1000000000000', // 1e12
false,
UnderlyingAsset['eth:ctgbp']
),
erc7984(
Expand All @@ -48,6 +56,9 @@ export const erc7984Tokens = [
'Zama Confidential WETH',
6,
'0xda9396b82634ea99243ce51258b6a5ae512d4893', // https://etherscan.io/token/0xda9396b82634Ea99243cE51258B6A5Ae512D4893
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH
'1000000000000', // 1e12
false,
UnderlyingAsset['eth:cweth']
),
erc7984(
Expand All @@ -56,6 +67,9 @@ export const erc7984Tokens = [
'Zama USDT',
6,
'0xae0207c757aa2b4019ad96edd0092ddc63ef0c50', // https://etherscan.io/token/0xAe0207C757Aa2B4019Ad96edD0092ddc63EF0c50
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
'1',
true,
UnderlyingAsset['eth:cusdt']
),
erc7984(
Expand All @@ -64,6 +78,9 @@ export const erc7984Tokens = [
'Zama USDC',
6,
'0xe978f22157048e5db8e5d07971376e86671672b2', // https://etherscan.io/token/0xe978f22157048e5db8e5d07971376e86671672b2
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
'1',
false,
UnderlyingAsset['eth:cusdc']
),

Expand All @@ -74,6 +91,9 @@ export const erc7984Tokens = [
'Zama Token Test 1',
6,
'0x7b1d59bbcd291daa59cb6c8c5bc04de1afc4aba1',
'0x7740f913dc24d4f9e1a72531372c3170452b2f87',
'1000000000000', // 1e12
false,
UnderlyingAsset['hteth:ctest1']
),
terc7984(
Expand All @@ -82,6 +102,9 @@ export const erc7984Tokens = [
'Zama USDT',
6,
'0x2debbe0487ef921df4457f9e36ed05be2df1ac75',
'0x51a63b5621d78de54d2f4d098a23a5a69e76f30b',
'1',
false,
UnderlyingAsset['hteth:cusdt']
),
];
11 changes: 10 additions & 1 deletion modules/statics/src/tokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ export type EosTokenConfig = BaseContractAddressConfig & {
contractAddress: string;
};
export type Erc20TokenConfig = BaseContractAddressConfig;
export type Erc7984TokenConfig = BaseContractAddressConfig;
export type Erc7984TokenConfig = BaseContractAddressConfig & {
underlyingErc20Address: string;
rate: string;
requiresApprovalReset: boolean;
isVetted: boolean;
};
export type TrxTokenConfig = BaseContractAddressConfig;
export type StellarTokenConfig = BaseNetworkConfig;

Expand Down Expand Up @@ -386,6 +391,10 @@ function getErc7984TokenConfig(coin: Erc7984Coin): Erc7984TokenConfig {
name: coin.fullName,
tokenContractAddress: coin.contractAddress.toString().toLowerCase(),
decimalPlaces: coin.decimalPlaces,
underlyingErc20Address: coin.underlyingErc20Address.toLowerCase(),
rate: coin.rate,
requiresApprovalReset: coin.requiresApprovalReset,
isVetted: coin.isVetted,
};
}

Expand Down
44 changes: 44 additions & 0 deletions modules/statics/test/unit/erc7984Tokens.ts
Original file line number Diff line number Diff line change
@@ -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 vetted Hoodi test pair', function () {
const hoodi = getErc7984('hteth:cusdt');
hoodi.network.name.should.equal(Networks.test.hoodi.name);
hoodi.isVetted.should.equal(true);
});

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('marks mainnet USDT wrapper as requiring approval reset', function () {
const coin = getErc7984('eth:cusdt');
coin.underlyingErc20Address.should.equal('0xdac17f958d2ee523a2206206994597c13d831ec7');
coin.requiresApprovalReset.should.equal(true);
coin.rate.should.equal('1');
});

it('exposes non-1 rates for wrappers that scale decimals', function () {
const coin = getErc7984('eth:cweth');
coin.rate.should.equal('1000000000000');
coin.underlyingErc20Address.should.equal('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2');
});

it('lowercases addresses on construction', function () {
const coin = getErc7984('eth:cusdc');
coin.contractAddress.should.equal(coin.contractAddress.toLowerCase());
coin.underlyingErc20Address.should.equal(coin.underlyingErc20Address.toLowerCase());
});
});
Loading