Skip to content
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
43 changes: 40 additions & 3 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -1287,6 +1311,9 @@ export function erc7984(
fullName,
network,
contractAddress,
underlyingErc20Address,
rate,
requiresApprovalReset,
prefix,
suffix,
features,
Expand All @@ -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
Expand All @@ -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 = '',
Expand All @@ -1338,7 +1372,10 @@ export function terc7984(
prefix,
suffix,
network,
primaryKeyCurve
primaryKeyCurve,
underlyingErc20Address,
rate,
requiresApprovalReset
);
}

Expand Down
14 changes: 10 additions & 4 deletions modules/statics/src/coins/erc7984Tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,6 +74,9 @@ export const erc7984Tokens = [
'Zama Token Test 1',
6,
'0x7b1d59bbcd291daa59cb6c8c5bc04de1afc4aba1',
'0x7740f913dc24d4f9e1a72531372c3170452b2f87',
'1000000000000', // 1e12
false,
UnderlyingAsset['hteth:ctest1']
),
terc7984(
Expand All @@ -82,6 +85,9 @@ export const erc7984Tokens = [
'Zama USDT',
6,
'0x2debbe0487ef921df4457f9e36ed05be2df1ac75',
'0x51a63b5621d78de54d2f4d098a23a5a69e76f30b',
'1',
false,
UnderlyingAsset['hteth:cusdt']
),
];
14 changes: 13 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 & {
/** 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;

Expand Down Expand Up @@ -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,
}),
};
}

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 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());
});
});