Skip to main content

The Contract

pragma language_version >= 0.17.0;

import CompactStandardLibrary;

// Initialization state
export ledger _isInitialized: Boolean;

// Token metadata
export sealed ledger _name: Opaque<"string">;
export sealed ledger _symbol: Opaque<"string">;
export sealed ledger _decimals: Uint<8>;

// Token balances and allowances
export ledger _balances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>;
export ledger _allowances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>;
export ledger _totalSupply: Uint<128>;

// Initialize the token (can only be called once)
export circuit initialize(
    name_: Opaque<"string">,
    symbol_: Opaque<"string">,
    decimals_: Uint<8>
): [] {
    assert(!_isInitialized, "Already initialized");
    _isInitialized = disclose(true);

    _name = disclose(name_);
    _symbol = disclose(symbol_);
    _decimals = disclose(decimals_);
}

// Get token name
export circuit name(): Opaque<"string"> {
    assert(_isInitialized, "Not initialized");
    return _name;
}

// Get token symbol
export circuit symbol(): Opaque<"string"> {
    assert(_isInitialized, "Not initialized");
    return _symbol;
}

// Get decimals
export circuit decimals(): Uint<8> {
    assert(_isInitialized, "Not initialized");
    return _decimals;
}

// Get total supply
export circuit totalSupply(): Uint<128> {
    assert(_isInitialized, "Not initialized");
    return _totalSupply;
}

// Get balance of an account
export circuit balanceOf(account: Either<ZswapCoinPublicKey, ContractAddress>): Uint<128> {
    assert(_isInitialized, "Not initialized");
    if (!_balances.member(disclose(account))) {
        return 0;
    }
    return _balances.lookup(disclose(account));
}

// Transfer tokens
export circuit transfer(to: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): Boolean {
    assert(_isInitialized, "Not initialized");
    const owner = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _transfer(owner, to, value);
    return true;
}

// Approve spender
export circuit approve(spender: Either<ZswapCoinPublicKey, ContractAddress>, value: Uint<128>): Boolean {
    assert(_isInitialized, "Not initialized");
    const owner = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _approve(owner, spender, value);
    return true;
}

// Get allowance
export circuit allowance(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
    assert(_isInitialized, "Not initialized");
    if (!_allowances.member(disclose(owner)) || !_allowances.lookup(owner).member(disclose(spender))) {
        return 0;
    }
    return _allowances.lookup(owner).lookup(disclose(spender));
}

// Transfer from approved account
export circuit transferFrom(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    value: Uint<128>
): Boolean {
    assert(_isInitialized, "Not initialized");
    const spender = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _spendAllowance(from, spender, value);
    _transfer(from, to, value);
    return true;
}

// Internal transfer function
circuit _transfer(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    value: Uint<128>
): [] {
    assert(!isKeyOrAddressZero(from), "Invalid sender");
    assert(!isKeyOrAddressZero(to), "Invalid receiver");

    const fromBal = balanceOf(from);
    assert(fromBal >= value, "Insufficient balance");
    _balances.insert(disclose(from), disclose(fromBal - value as Uint<128>));

    const toBal = balanceOf(to);
    _balances.insert(disclose(to), disclose(toBal + value as Uint<128>));
}

// Internal approve function
circuit _approve(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    value: Uint<128>
): [] {
    assert(!isKeyOrAddressZero(owner), "Invalid owner");
    assert(!isKeyOrAddressZero(spender), "Invalid spender");

    if (!_allowances.member(disclose(owner))) {
        _allowances.insert(disclose(owner), default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>);
    }
    _allowances.lookup(owner).insert(disclose(spender), disclose(value));
}

// Spend allowance
circuit _spendAllowance(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    value: Uint<128>
): [] {
    const currentAllowance = allowance(owner, spender);
    const MAX_UINT128 = 340282366920938463463374607431768211455;

    if (currentAllowance < MAX_UINT128) {
        assert(currentAllowance >= value, "Insufficient allowance");
        _approve(owner, spender, currentAllowance - value as Uint<128>);
    }
}

// Mint new tokens
export circuit _mint(
    account: Either<ZswapCoinPublicKey, ContractAddress>,
    value: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    assert(!isKeyOrAddressZero(account), "Invalid receiver");

    const MAX_UINT128 = 340282366920938463463374607431768211455;
    assert(MAX_UINT128 - _totalSupply >= value, "Mint overflow");

    _totalSupply = disclose(_totalSupply + value as Uint<128>);

    const balance = balanceOf(account);
    _balances.insert(disclose(account), disclose(balance + value as Uint<128>));
}

// Burn tokens
export circuit _burn(
    account: Either<ZswapCoinPublicKey, ContractAddress>,
    value: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    assert(!isKeyOrAddressZero(account), "Invalid sender");

    const balance = balanceOf(account);
    assert(balance >= value, "Insufficient balance");
    _balances.insert(disclose(account), disclose(balance - value as Uint<128>));

    _totalSupply = disclose(_totalSupply - value as Uint<128>);
}

// Helper Functions
// These would typically be imported from OpenZeppelin's Utils module
circuit isKeyOrAddressZero(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return isContractAddress(keyOrAddress)
        ? default<ContractAddress> == keyOrAddress.right
        : default<ZswapCoinPublicKey> == keyOrAddress.left;
}

circuit isContractAddress(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return !keyOrAddress.is_left;
}

circuit burnAddress(): Either<ZswapCoinPublicKey, ContractAddress> {
    return left<ZswapCoinPublicKey, ContractAddress>(default<ZswapCoinPublicKey>);
}

How It Works

This ERC20 token implementation combines all the patterns from the basics tutorials. For detailed explanations of each concept, see the linked tutorials.

Initialization Protection

The contract uses an initialization guard to ensure initialize() can only be called once:
export ledger _isInitialized: Boolean;

export circuit initialize(...): [] {
    assert(!_isInitialized, "Already initialized");
    _isInitialized = disclose(true);
    // ... set metadata
}
All public circuits check assert(_isInitialized, "Not initialized") before executing.

Core Token Features

Balance Tracking - Uses Map storage to track token amounts per account. See Mappings tutorial. Transfers - Implements transfer() and internal _transfer() with balance validation and overflow protection. See Transfer tutorial for details. Approvals - Nested Map structure (owner → spender → amount) enables delegated transfers. See Approval tutorial for granting permissions and Allowance tutorial for spending approved tokens. Minting & Burning - Internal _mint() and _burn() functions manage token supply with overflow checks. See Minting tutorial and Burning tutorial for implementation details.

ERC20-Specific Notes

  • Uint<128> amounts: Compact doesn’t support Uint<256> due to circuit constraints (max: 340 undecillion tokens)
  • Boolean returns: Functions return true for ERC20 compatibility
  • Decimals: Usually 18 for currency tokens, set once during initialization
  • Infinite approval: Set allowance to MAX_UINT128 for permanent approval

Helper Functions

The contract uses utility functions for address validation:
circuit isKeyOrAddressZero(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return isContractAddress(keyOrAddress)
        ? default<ContractAddress> == keyOrAddress.right
        : default<ZswapCoinPublicKey> == keyOrAddress.left;
}

circuit burnAddress(): Either<ZswapCoinPublicKey, ContractAddress> {
    return left<ZswapCoinPublicKey, ContractAddress>(default<ZswapCoinPublicKey>);
}
  • isKeyOrAddressZero: Checks if an address is the zero/default address. Returns true for uninitialized addresses.
  • burnAddress: Returns the burn address, represented as the default ZswapCoinPublicKey. Used in minting (from burn address) and burning (to burn address).
In production OpenZeppelin contracts, these utilities are imported from the Utils module. The implementations shown here match the OpenZeppelin Compact Contracts v0.0.1-alpha.0 specification.

Try It Yourself

1. Create project structure:
mkdir my-token && cd my-token
mkdir -p contracts
2. Save the contract:Create contracts/my-token.compact with the code above.3. Initialize your token:In your contract constructor:
circuit constructor(): [] {
    initialize("My Token", "MTK", 18);
    // Mint initial supply to deployer
    const deployer = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _mint(deployer, 1000000 * (10 ** 18)); // 1 million tokens
}
4. Compile:
compact compile contracts/my-token.compact contracts/managed/my-token

What’s Next