Skip to main content

Deploy a Privacy-Enabled Token

Specter provides the GhostERC20Factory contract to deploy ERC-20 tokens that are automatically integrated with the Ghost Protocol privacy system. Tokens deployed through this factory can be committed and revealed through the CommitRevealVault, enabling private transfers with zero-knowledge proofs.

Overview

The deployment flow is:

  1. Deploy — Call deployToken() on the GhostERC20Factory to create a new GhostERC20 token contract via CREATE2.
  2. Register — The factory automatically registers the new token with AssetGuard, authorizing it for use in the commit/reveal system.
  3. Enable — Call enableGhost() on the newly deployed token to finalize its connection to the privacy infrastructure.

Once enabled, the token can be committed into the CommitRevealVault and revealed with zero-knowledge proofs, just like native GHOST.

Contract Addresses

ContractAddress
GhostERC20Factory0xE842ffe639a770a162e0b7EB9f274E49aCA8Fb95
AssetGuard0x12d5a4d9Db0607312Fc8F8eE51FDf18D40794aD1
CommitRevealVault0x908aA11Dc9F2e2C3F69892acaDE112e831c0a14a

Step 1: Deploy the Token

Call deployToken on the GhostERC20Factory:

function deployToken(
string memory name,
string memory symbol,
uint8 decimals,
bytes32 salt
) external returns (address tokenAddress);

Parameters

ParameterTypeDescription
namestringThe full name of the token (e.g., "Ghost Wrapped DAI").
symbolstringThe ticker symbol (e.g., "gDAI"). Convention is to prefix with g.
decimalsuint8Number of decimal places. Use 18 for standard EVM tokens, 6 for stablecoin equivalents.
saltbytes32A unique salt for CREATE2 deterministic deployment. Use a random value or a domain-specific identifier.

Example: Deploy with ethers.js

import { ethers } from "ethers";

const FACTORY_ADDRESS = "0xE842ffe639a770a162e0b7EB9f274E49aCA8Fb95";

const factoryAbi = [
"function deployToken(string name, string symbol, uint8 decimals, bytes32 salt) external returns (address)"
];

const provider = new ethers.JsonRpcProvider("https://testnet.specterchain.com");
const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const factory = new ethers.Contract(FACTORY_ADDRESS, factoryAbi, signer);

const salt = ethers.randomBytes(32);

const tx = await factory.deployToken("Ghost Wrapped DAI", "gDAI", 18, salt);
const receipt = await tx.wait();

// The new token address is emitted in the TokenDeployed event
const event = receipt.logs.find(
(log) => log.address.toLowerCase() === FACTORY_ADDRESS.toLowerCase()
);
console.log("Token deployed at:", event.args.tokenAddress);

Deterministic Addresses with CREATE2

Because the factory uses CREATE2, the deployed token address is deterministic given the same name, symbol, decimals, and salt. You can pre-compute the address off-chain:

const initCodeHash = ethers.keccak256(
ethers.concat([
ghostERC20Bytecode,
ethers.AbiCoder.defaultAbiCoder().encode(
["string", "string", "uint8"],
["Ghost Wrapped DAI", "gDAI", 18]
),
])
);

const tokenAddress = ethers.getCreate2Address(
FACTORY_ADDRESS,
salt,
initCodeHash
);

Step 2: Automatic AssetGuard Registration

When the factory deploys a token, it automatically calls AssetGuard.registerToken() to whitelist the new token for use in the CommitRevealVault. You do not need to perform this step manually.

The AssetGuard contract (0x12d5a4d9Db0607312Fc8F8eE51FDf18D40794aD1) maintains a registry of all tokens authorized to interact with the privacy system. Only registered tokens can be committed and revealed.

You can verify registration:

const assetGuardAbi = ["function isRegistered(address token) view returns (bool)"];
const assetGuard = new ethers.Contract(
"0x12d5a4d9Db0607312Fc8F8eE51FDf18D40794aD1",
assetGuardAbi,
provider
);

const registered = await assetGuard.isRegistered(tokenAddress);
console.log("Registered:", registered); // true

Step 3: Enable Ghost Mode

After deployment and registration, call enableGhost() on the token contract itself to finalize the privacy integration:

const ghostTokenAbi = [
"function enableGhost() external",
"function ghostEnabled() view returns (bool)"
];

const ghostToken = new ethers.Contract(tokenAddress, ghostTokenAbi, signer);

const tx = await ghostToken.enableGhost();
await tx.wait();

const enabled = await ghostToken.ghostEnabled();
console.log("Ghost enabled:", enabled); // true

enableGhost() configures the token's internal hooks so that when tokens are committed to the CommitRevealVault, they are burned from the sender, and when revealed, they are minted to the recipient. This mirrors the native GHOST behavior but for ERC-20 tokens.

caution

Only the token deployer (or a designated admin) can call enableGhost(). This is a one-time, irreversible operation.

Step 4: Use the Token with CommitRevealVault

Once the token is ghost-enabled, it works seamlessly with the CommitRevealVault:

Commit (Deposit into Privacy Pool)

const vaultAbi = [
"function commit(bytes32 commitment, address token, uint256 amount) external"
];
const vault = new ethers.Contract(
"0x908aA11Dc9F2e2C3F69892acaDE112e831c0a14a",
vaultAbi,
signer
);

// First, approve the vault to spend your tokens
const approveTx = await ghostToken.approve(vault.target, amount);
await approveTx.wait();

// Then commit
const commitment = /* your computed Poseidon commitment */;
const commitTx = await vault.commit(commitment, tokenAddress, amount);
await commitTx.wait();

Reveal (Withdraw from Privacy Pool)

The reveal flow is identical to native GHOST reveals — generate a Groth16 proof demonstrating knowledge of a valid commitment in the Merkle tree, then submit the proof along with the nullifier. See Client-Side Proofs for details on proof generation.

Token Naming Conventions

Existing privacy tokens on Specter follow a g-prefix convention:

TokenAddressUnderlying
gLABS0x062f8a68f6386c1b448b3379abd369825bec9aa2LABS
gUSDC0x65c9091a6A45Db302a343AF460657C298FAA222DUSDC
gWETH0x923295a3e3bE5eDe29Fc408A507dA057ee044E81WETH
gVIRTUAL0xaF12d2f962179274f243986604F97b961a4f4CfcVIRTUAL

Troubleshooting

  • deployToken reverts: Ensure the salt has not been used before. Each unique combination of constructor parameters and salt produces one address.
  • enableGhost reverts with unauthorized: Only the deployer (msg.sender of deployToken) can call enableGhost().
  • Token not appearing in webapp: The webapp reads tokens from its config.js. You may need to add your token's address and tokenIdHash to the configuration. See Webapp Configuration.
  • Commit reverts with "token not registered": The AssetGuard registration may have failed. Check isRegistered() and contact the team if needed.