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:
- Deploy — Call
deployToken()on theGhostERC20Factoryto create a new GhostERC20 token contract viaCREATE2. - Register — The factory automatically registers the new token with
AssetGuard, authorizing it for use in the commit/reveal system. - 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
| Contract | Address |
|---|---|
| GhostERC20Factory | 0xE842ffe639a770a162e0b7EB9f274E49aCA8Fb95 |
| AssetGuard | 0x12d5a4d9Db0607312Fc8F8eE51FDf18D40794aD1 |
| CommitRevealVault | 0x908aA11Dc9F2e2C3F69892acaDE112e831c0a14a |
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
| Parameter | Type | Description |
|---|---|---|
name | string | The full name of the token (e.g., "Ghost Wrapped DAI"). |
symbol | string | The ticker symbol (e.g., "gDAI"). Convention is to prefix with g. |
decimals | uint8 | Number of decimal places. Use 18 for standard EVM tokens, 6 for stablecoin equivalents. |
salt | bytes32 | A 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.
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:
| Token | Address | Underlying |
|---|---|---|
| gLABS | 0x062f8a68f6386c1b448b3379abd369825bec9aa2 | LABS |
| gUSDC | 0x65c9091a6A45Db302a343AF460657C298FAA222D | USDC |
| gWETH | 0x923295a3e3bE5eDe29Fc408A507dA057ee044E81 | WETH |
| gVIRTUAL | 0xaF12d2f962179274f243986604F97b961a4f4Cfc | VIRTUAL |
Troubleshooting
deployTokenreverts: Ensure thesalthas not been used before. Each unique combination of constructor parameters and salt produces one address.enableGhostreverts with unauthorized: Only the deployer (msg.sender ofdeployToken) can callenableGhost().- Token not appearing in webapp: The webapp reads tokens from its
config.js. You may need to add your token's address andtokenIdHashto the configuration. See Webapp Configuration. - Commit reverts with "token not registered": The
AssetGuardregistration may have failed. CheckisRegistered()and contact the team if needed.