Skip to main content
Mint authority is gated by the witness-derived admin keypair from Access Control. The admin proves knowledge of a witness secret whose hash matches the stored _admin public key.

The Contract

pragma language_version >= 0.23;

import CompactStandardLibrary;

// Admin keypair (witness-derived)
struct AdminSecretKey { bytes: Bytes<32>; }
struct AdminPublicKey { bytes: Bytes<32>; }
witness getAdminSecret(): AdminSecretKey;

// User keypair (witness-derived) — used as a balance index
struct UserSecretKey { bytes: Bytes<32>; }
struct UserPublicKey { bytes: Bytes<32>; }
witness getUserSecret(): UserSecretKey;

// Ledger state
export ledger _admin: AdminPublicKey;
export ledger _balances: Map<UserPublicKey, Uint<128>>;
export ledger _totalSupply: Uint<128>;

constructor(initialAdmin: AdminPublicKey) {
    _admin = disclose(initialAdmin);
}

// Key derivation
export circuit deriveAdminPublicKey(sk: AdminSecretKey): AdminPublicKey {
    return AdminPublicKey {
        bytes: persistentHash<Vector<2, Bytes<32>>>([
            pad(32, "myapp:admin:v1"),
            sk.bytes
        ])
    };
}

export circuit deriveUserPublicKey(sk: UserSecretKey): UserPublicKey {
    return UserPublicKey {
        bytes: persistentHash<Vector<2, Bytes<32>>>([
            pad(32, "myapp:user:v1"),
            sk.bytes
        ])
    };
}

// Read total supply
export circuit getTotalSupply(): Uint<128> {
    return _totalSupply;
}

// Mint — admin only
export circuit mint(account: UserPublicKey, amount: Uint<128>): [] {
    assert(
        _admin == deriveAdminPublicKey(getAdminSecret()),
        "Only admin may mint"
    );
    _mint(account, amount);
}

// Internal mint logic
circuit _mint(account: UserPublicKey, amount: Uint<128>): [] {
    const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;

    // Check total supply won't overflow
    assert(_totalSupply <= MAX_UINT128 - amount, "Total supply overflow");
    _totalSupply = disclose((_totalSupply + amount) as Uint<128>);

    // Check recipient's balance won't overflow
    const currentBalance = getBalance(account);
    assert(currentBalance <= MAX_UINT128 - amount, "Balance overflow");
    _balances.insert(
        disclose(account),
        disclose((currentBalance + amount) as Uint<128>)
    );
}

// Helper
circuit getBalance(account: UserPublicKey): Uint<128> {
    if (!_balances.member(disclose(account))) { return 0; }
    return _balances.lookup(disclose(account));
}

How It Works

Admin authentication

assert(
    _admin == deriveAdminPublicKey(getAdminSecret()),
    "Only admin may mint"
);
The contract stores the admin’s public key. To mint, the caller must produce a ZK proof that they know a witness secret whose hash equals that public key. Reading _admin off chain gives an attacker the hash, not the secret — preimage resistance means they cannot forge the proof.

Total supply invariant

assert(_totalSupply <= MAX_UINT128 - amount, "Total supply overflow");
_totalSupply = disclose((_totalSupply + amount) as Uint<128>);
sum(all balances) == _totalSupply is maintained by always updating both sides together. Minting increases both; burning (Burning) decreases both; transferring preserves both.

Cast back to Uint<128> on store

_totalSupply = disclose((_totalSupply + amount) as Uint<128>);
Arithmetic widens; after the overflow assertion we cast back to Uint<128> before writing. See Overflow Protection.

Privacy Note

Each mint discloses account and amount on the public ledger. The full participant set and amount of every mint is observable to anyone reading the chain. This contract is suitable for transparent registries (game items, public rewards) but not for private finance.

What’s Next

Burning Tokens

Learn how to destroy tokens

ERC20 Token

Complete token with minting