> ## Documentation Index
> Fetch the complete documentation index at: https://compact-by-example.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Fungible Token (ERC20)

> Full ERC20-style fungible token in Compact: transfer, approve, transferFrom, mint, and burn with witness-derived keypair authentication.

<Warning>
  **This is an *account-model* token. Balances and transfers are visible on
  chain.** Every transfer publishes (sender, recipient, amount). The
  participant graph is fully observable.

  Use this pattern for transparent registries (game economies, public reward
  programs) — **not** for private finance. For private payments, use
  Midnight's native shielded primitives (`sendShielded`, `mintShieldedToken`,
  etc.) which keep balances and counterparties in ZK-committed UTXOs.
</Warning>

<Warning>
  **Authentication uses witness-derived keypairs**, not `ownPublicKey()`.
  See [Access Control](/basics/access-control) for the rationale and
  [Transfer](/basics/transfer) for the building block this contract uses.
</Warning>

## The Contract

<Accordion title="View Contract Code">
  ```compact theme={null}
  pragma language_version >= 0.23;

  import CompactStandardLibrary;

  // Caller keypair (witness-derived) — used to authenticate users
  struct UserSecretKey { bytes: Bytes<32>; }
  struct UserPublicKey { bytes: Bytes<32>; }
  witness getUserSecret(): UserSecretKey;

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

  // Token metadata (sealed: settable only in the constructor)
  export sealed ledger _name: Opaque<"string">;
  export sealed ledger _symbol: Opaque<"string">;
  export sealed ledger _decimals: Uint<8>;

  // On-chain admin and ledger state
  export ledger _admin: AdminPublicKey;
  export ledger _balances: Map<UserPublicKey, Uint<128>>;
  export ledger _allowances: Map<UserPublicKey, Map<UserPublicKey, Uint<128>>>;
  export ledger _totalSupply: Uint<128>;

  constructor(
      initialAdmin: AdminPublicKey,
      name_: Opaque<"string">,
      symbol_: Opaque<"string">,
      decimals_: Uint<8>
  ) {
      _admin = disclose(initialAdmin);
      _name = disclose(name_);
      _symbol = disclose(symbol_);
      _decimals = disclose(decimals_);
  }

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

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

  // Metadata readers
  export circuit name(): Opaque<"string"> { return _name; }
  export circuit symbol(): Opaque<"string"> { return _symbol; }
  export circuit decimals(): Uint<8> { return _decimals; }
  export circuit totalSupply(): Uint<128> { return _totalSupply; }

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

  // Transfer — caller authenticates as the sender
  export circuit transfer(to: UserPublicKey, value: Uint<128>): Boolean {
      const owner = disclose(deriveUserPublicKey(getUserSecret()));
      _transfer(owner, to, value);
      return true;
  }

  // Approve — caller authenticates as the owner
  export circuit approve(spender: UserPublicKey, value: Uint<128>): Boolean {
      const owner = disclose(deriveUserPublicKey(getUserSecret()));
      _approve(owner, spender, value);
      return true;
  }

  // Read remaining allowance
  export circuit allowance(owner: UserPublicKey, spender: UserPublicKey): Uint<128> {
      if (!_allowances.member(disclose(owner))) { return 0; }
      if (!_allowances.lookup(disclose(owner)).member(disclose(spender))) { return 0; }
      return _allowances.lookup(disclose(owner)).lookup(disclose(spender));
  }

  // transferFrom — caller authenticates as the spender
  export circuit transferFrom(
      owner: UserPublicKey,
      to: UserPublicKey,
      value: Uint<128>
  ): Boolean {
      const spender = disclose(deriveUserPublicKey(getUserSecret()));
      _spendAllowance(owner, spender, value);
      _transfer(owner, to, value);
      return true;
  }

  // Admin: mint
  export circuit mint(account: UserPublicKey, value: Uint<128>): [] {
      assertAdmin();
      _mint(account, value);
  }

  // Admin: burn (forceful)
  export circuit adminBurn(account: UserPublicKey, value: Uint<128>): [] {
      assertAdmin();
      _burn(account, value);
  }

  // Self-burn: caller burns their own tokens
  export circuit burn(value: Uint<128>): [] {
      const account = disclose(deriveUserPublicKey(getUserSecret()));
      _burn(account, value);
  }

  // Internals
  circuit assertAdmin(): [] {
      assert(_admin == deriveAdminPublicKey(getAdminSecret()), "Not admin");
  }

  circuit _transfer(
      sender: UserPublicKey,
      to: UserPublicKey,
      value: Uint<128>
  ): [] {
      const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
      const fromBal = balanceOf(sender);
      assert(fromBal >= value, "Insufficient balance");
      const toBal = balanceOf(to);
      assert(toBal <= MAX_UINT128 - value, "Balance overflow");
      _balances.insert(disclose(sender), disclose(fromBal - value));
      _balances.insert(disclose(to), disclose((toBal + value) as Uint<128>));
  }

  circuit _approve(
      owner: UserPublicKey,
      spender: UserPublicKey,
      value: Uint<128>
  ): [] {
      if (!_allowances.member(disclose(owner))) {
          _allowances.insert(disclose(owner), default<Map<UserPublicKey, Uint<128>>>);
      }
      _allowances.lookup(disclose(owner)).insert(disclose(spender), disclose(value));
  }

  circuit _spendAllowance(
      owner: UserPublicKey,
      spender: UserPublicKey,
      value: Uint<128>
  ): [] {
      const current = allowance(owner, spender);
      const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
      if (current < MAX_UINT128) {
          assert(current >= value, "Insufficient allowance");
          _approve(owner, spender, current - value);
      }
  }

  circuit _mint(account: UserPublicKey, value: Uint<128>): [] {
      const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
      assert(_totalSupply <= MAX_UINT128 - value, "Total supply overflow");
      _totalSupply = disclose((_totalSupply + value) as Uint<128>);
      const bal = balanceOf(account);
      assert(bal <= MAX_UINT128 - value, "Balance overflow");
      _balances.insert(disclose(account), disclose((bal + value) as Uint<128>));
  }

  circuit _burn(account: UserPublicKey, value: Uint<128>): [] {
      const bal = balanceOf(account);
      assert(bal >= value, "Insufficient balance");
      assert(_totalSupply >= value, "Total supply underflow");
      _balances.insert(disclose(account), disclose(bal - value));
      _totalSupply = disclose(_totalSupply - value);
  }
  ```
</Accordion>

## How It Works

This ERC20 implementation combines the patterns from the basics tutorials.
For detailed explanations, follow the linked sub-tutorials.

### Authentication

Every state-changing circuit authenticates the caller by **proving knowledge
of a witness secret** whose `persistentHash` equals a stored public key:

* `transfer`, `approve`, `burn` → caller authenticates as a `UserPublicKey`
* `transferFrom` → caller authenticates as the spender
* `mint`, `adminBurn` → caller authenticates as the admin

See [Access Control](/basics/access-control) for why `ownPublicKey()`
**cannot** be used here.

### Initialization

```compact theme={null}
constructor(
    initialAdmin: AdminPublicKey,
    name_: Opaque<"string">,
    symbol_: Opaque<"string">,
    decimals_: Uint<8>
)
```

The constructor seeds `_admin` and the `sealed` metadata fields (`_name`,
`_symbol`, `_decimals`). `sealed` ledger fields can only be written from the
constructor, so they cannot be modified after deployment — exactly the
"set once, read forever" semantics ERC20 metadata wants.

### Total supply invariant

```compact theme={null}
_totalSupply = disclose((_totalSupply + value) as Uint<128>);
```

`sum(all balances) == _totalSupply` is preserved: `mint` increases both,
`_burn` decreases both, `_transfer` preserves both. See
[Overflow Protection](/basics/overflow-protection).

### Why no `isKeyOrAddressZero` / `burnAddress` helpers?

`UserPublicKey` is a `Bytes<32>` hash of a witness secret. There is no
canonical "zero address" — the burn happens by deducting from a balance
without crediting it elsewhere (`_burn`), and admin authority is enforced via
the keypair check, not by checking for the default value.

### Privacy trade-off

The keypair scheme authenticates the caller but does not hide them.
`_balances` stores `UserPublicKey → amount`; every write reveals which key
moved and by how much. For genuinely private tokens, use Midnight's shielded
primitives — they're built on UTXOs and ZK commitments and don't expose the
participant graph.

## Try It Yourself

<Accordion title="Build and Deploy">
  **1. Create project structure:**

  ```bash theme={null}
  mkdir my-token && cd my-token
  mkdir -p contracts
  ```

  **2. Save the contract** at `contracts/my-token.compact`.

  **3. Compile:**

  ```bash theme={null}
  compact compile contracts/my-token.compact contracts/managed/my-token
  ```

  **4. Deploy** with the four constructor arguments (admin pubkey, name, symbol,
  decimals). The witness side is wired by your DApp:

  * `getAdminSecret()` — the admin's private `Bytes<32>` (kept in DApp state)
  * `getUserSecret()` — each user's private `Bytes<32>`
  * `initialAdmin: AdminPublicKey` — computed off chain as
    `persistentHash([pad(32, "erc20:admin:v1"), adminSecretBytes])`
</Accordion>

## What's Next

<CardGroup cols={2}>
  <Card title="NFT (ERC721)" icon="image" href="/applications/erc721-nft">
    Create unique, non-fungible tokens
  </Card>

  <Card title="Multi-Token (ERC1155)" icon="layer-group" href="/applications/erc1155-multitoken">
    Manage multiple token types in one contract
  </Card>
</CardGroup>
