GhostMint Module
The x/ghostmint module is the core protocol-level primitive that makes Specter's native GHOST privacy possible. It is a custom Cosmos SDK module that bridges EVM smart contract execution with Cosmos-native token minting, allowing authorized EVM contracts to mint native GHOST tokens directly through a precompiled contract.
Why GhostMint Exists
In a typical privacy protocol on Ethereum, users deposit ETH or tokens into a shielded pool contract. When they withdraw, the contract transfers tokens from its own balance back to the user. This means the contract must hold a pool of tokens, and the pool balance itself leaks information about the privacy set.
Specter takes a fundamentally different approach. When a user deposits GHOST into the privacy pool, the tokens are burned. When they withdraw, new GHOST tokens are minted at the protocol level. There is no pool balance to observe. The minting happens through the Cosmos SDK's x/bank module, making it indistinguishable from any other native token operation.
This is only possible because Specter controls the full stack — the EVM execution environment, the Cosmos application layer, and the consensus engine.
Architecture
┌──────────────────────────────────┐
│ NativeAssetHandler.sol │ ← Authorized EVM contract
│ (calls precompile at 0x808) │
└──────────────┬───────────────────┘
│ mintNativeTo(address, uint256)
▼
┌──────────────────────────────────┐
│ GhostMint Precompile │ ← EVM precompile at 0x0...0808
│ (0x0000...000000000808) │
│ Validates caller == NAH │
└──────────────┬───────────────────┘
│ MintNativeTo(ctx, recipient, amount)
▼
┌──────────────────────────────────┐
│ x/ghostmint Keeper │ ← Cosmos SDK module
│ Wraps x/bank module │
└──────────────┬───────────────────┘
│ MintCoins → SendCoinsFromModuleToAccount
▼
┌──────────────────────────────────┐
│ x/bank Module │ ← Native Cosmos token ledger
│ (aghost balance updated) │
└──────────────────────────────────┘
Module Types
package types
const (
// ModuleName defines the module name
ModuleName = "ghostmint"
// StoreKey defines the primary module store key
StoreKey = ModuleName
// RouterKey defines the module's message routing key
RouterKey = ModuleName
)
// NativeDenom is the denomination of the native token that can be minted
const NativeDenom = "aghost"
Keeper
The GhostMint keeper is a thin wrapper around the x/bank module's keeper. It exposes a single operation: mint native tokens and send them to a recipient address.
MintNativeTo
// MintNativeTo mints native GHOST tokens and sends them to the recipient.
// This is the only way new GHOST enters circulation post-genesis.
func (k Keeper) MintNativeTo(
ctx sdk.Context,
recipient sdk.AccAddress,
amount sdk.Int,
) error {
// 1. Validate amount is positive
if !amount.IsPositive() {
return ErrInvalidAmount
}
// 2. Validate recipient address
if recipient.Empty() {
return ErrInvalidRecipient
}
// 3. Create the coin to mint
coins := sdk.NewCoins(sdk.NewCoin(NativeDenom, amount))
// 4. Mint coins to the ghostmint module account
if err := k.bankKeeper.MintCoins(ctx, ModuleName, coins); err != nil {
return err
}
// 5. Send coins from module account to recipient
if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, ModuleName, recipient, coins); err != nil {
return err
}
return nil
}
Execution Flow
- Validate amount — The amount must be a positive integer. Zero or negative amounts are rejected with
ErrInvalidAmount. - Validate recipient — The recipient address must be a valid, non-empty Cosmos account address. Invalid recipients are rejected with
ErrInvalidRecipient. - Mint to module account — Coins are first minted to the
ghostmintmodule's own account usingx/bank.MintCoins. This increases the total supply. - Transfer to recipient — Coins are then transferred from the module account to the recipient using
x/bank.SendCoinsFromModuleToAccount.
Errors
| Error | Code | Description |
|---|---|---|
ErrInvalidAmount | 2 | Amount is zero, negative, or otherwise invalid |
ErrInvalidRecipient | 3 | Recipient address is empty or malformed |
ErrUnauthorized | 4 | Caller is not the authorized NativeAssetHandler contract |
var (
ErrInvalidAmount = errorsmod.Register(ModuleName, 2, "invalid amount")
ErrInvalidRecipient = errorsmod.Register(ModuleName, 3, "invalid recipient")
ErrUnauthorized = errorsmod.Register(ModuleName, 4, "unauthorized")
)
EVM Precompile
The GhostMint module exposes its functionality to the EVM through a precompiled contract — a special contract that exists at a fixed address and executes native Go code rather than EVM bytecode.
Precompile Address
0x0000000000000000000000000000000000000808
This is a reserved address in the EVM address space. Any CALL to this address triggers the GhostMint precompile rather than executing EVM bytecode.
Interface
The precompile exposes a single function:
// Solidity interface for the GhostMint precompile
interface IGhostMint {
/// @notice Mints native GHOST tokens and sends them to the recipient.
/// @param recipient The EVM address to receive the minted tokens.
/// @param amount The amount of aghost (smallest unit) to mint.
/// @dev Only callable by the authorized NativeAssetHandler contract.
function mintNativeTo(address recipient, uint256 amount) external;
}
ABI Encoding
The function selector for mintNativeTo(address,uint256) is computed as:
keccak256("mintNativeTo(address,uint256)") = 0x... (first 4 bytes)
A call to the precompile is ABI-encoded as:
[4-byte selector][32-byte address (left-padded)][32-byte uint256 amount]
Authorization
The precompile enforces a strict authorization check: only the NativeAssetHandler contract is allowed to call mintNativeTo. This is enforced at the protocol level — the precompile checks msg.sender against the authorized address before executing.
func (p *Precompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
// Reject static calls (read-only context)
if readonly {
return nil, vm.ErrWriteProtection
}
// Verify caller is the authorized NativeAssetHandler
if contract.CallerAddress != p.authorizedCaller {
return nil, ErrUnauthorized
}
// Parse ABI-encoded arguments
recipient, amount, err := parseArgs(contract.Input)
if err != nil {
return nil, err
}
// Convert EVM address to Cosmos address
cosmosAddr := sdk.AccAddress(recipient.Bytes())
// Execute the mint
if err := p.keeper.MintNativeTo(evm.StateDB.GetContext(), cosmosAddr, amount); err != nil {
return nil, err
}
return nil, nil
}
This means:
- Random EOAs (externally owned accounts) cannot call the precompile.
- Arbitrary smart contracts cannot call the precompile.
- Only the single, protocol-designated NativeAssetHandler contract can trigger minting.
- The authorized caller address is set at chain initialization and cannot be changed without a governance upgrade.
NativeAssetHandler Contract
The NativeAssetHandler is a Solidity smart contract deployed on the Specter EVM. It is the sole contract authorized to interact with the GhostMint precompile. It serves as the bridge between the GHOST privacy protocol (which handles zero-knowledge proof verification, commitment tracking, and nullifier management) and the native minting layer.
When a user completes a valid withdrawal from the privacy pool:
- The privacy protocol contract verifies the zero-knowledge proof.
- It calls the NativeAssetHandler to release funds.
- The NativeAssetHandler calls
mintNativeToon the GhostMint precompile. - The precompile invokes the
x/ghostmintkeeper, which mints native GHOST viax/bank. - The newly minted GHOST arrives in the recipient's account as native tokens.
Security Considerations
Single Point of Authorization
The entire minting capability is gated behind a single authorized contract. This is a deliberate design choice:
- Pro: Minimal attack surface. Only one contract can trigger minting, and that contract's logic is auditable.
- Pro: No governance overhead for individual mints — the authorization is baked into the protocol.
- Con: If the NativeAssetHandler contract has a vulnerability, it could be exploited to mint arbitrary amounts. This is mitigated by extensive auditing and the simplicity of the handler's logic.
Supply Integrity
Every mint operation goes through x/bank.MintCoins, which properly updates the chain's total supply tracking. This means:
- The total supply reported by the chain is always accurate.
- Block explorers and analytics tools can track minting events.
- There is no hidden or unaccounted-for token creation.
Immutability
The authorized caller address for the precompile is set at the protocol level. Changing it requires a coordinated software upgrade, not just a transaction. This prevents any single actor from redirecting minting authority.
Querying GhostMint State
Total Minted Supply
Since GhostMint uses x/bank under the hood, you can query the total supply of GHOST:
# Query total supply of aghost
umbralined query bank total --denom aghost
# Query the ghostmint module account balance (should be 0 if all mints are sent)
umbralined query bank balances $(umbralined keys show ghostmint-module -a)
EVM-Side Queries
From the EVM side, the minting is transparent — the recipient simply sees an increased native balance:
const { ethers } = require("ethers");
const provider = new ethers.JsonRpcProvider("https://testnet.specterchain.com");
// Check balance after a withdrawal (mint)
const balance = await provider.getBalance("0xRecipientAddress");
console.log("Balance:", ethers.formatEther(balance), "GHOST");
Relationship to the Privacy Protocol
The GhostMint module is one half of the privacy equation:
| Operation | Module | Direction |
|---|---|---|
| Deposit (enter privacy pool) | x/bank burn (via EVM) | GHOST leaves circulation |
| Withdrawal (exit privacy pool) | x/ghostmint mint (via precompile) | GHOST enters circulation |
The total circulating supply of GHOST remains constant (assuming equal deposits and withdrawals). The module does not create inflation — it re-materializes tokens that were previously burned during deposits.