Skip to main content

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

  1. Validate amount — The amount must be a positive integer. Zero or negative amounts are rejected with ErrInvalidAmount.
  2. Validate recipient — The recipient address must be a valid, non-empty Cosmos account address. Invalid recipients are rejected with ErrInvalidRecipient.
  3. Mint to module account — Coins are first minted to the ghostmint module's own account using x/bank.MintCoins. This increases the total supply.
  4. Transfer to recipient — Coins are then transferred from the module account to the recipient using x/bank.SendCoinsFromModuleToAccount.

Errors

ErrorCodeDescription
ErrInvalidAmount2Amount is zero, negative, or otherwise invalid
ErrInvalidRecipient3Recipient address is empty or malformed
ErrUnauthorized4Caller 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:

  1. The privacy protocol contract verifies the zero-knowledge proof.
  2. It calls the NativeAssetHandler to release funds.
  3. The NativeAssetHandler calls mintNativeTo on the GhostMint precompile.
  4. The precompile invokes the x/ghostmint keeper, which mints native GHOST via x/bank.
  5. 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:

OperationModuleDirection
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.