Skip to main content

The Contract

pragma language_version >= 0.17.0;

import CompactStandardLibrary;

// Account balances
export ledger balances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>;

// Allowances: owner -> (spender -> amount)
export ledger allowances: Map<
    Either<ZswapCoinPublicKey, ContractAddress>,
    Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>
>;

// Approve spender to spend tokens on your behalf
export circuit approve(
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): Boolean {
    const owner = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _approve(owner, spender, amount);
    return true;
}

// Check how much spender is allowed to spend
export circuit allowance(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
    if (!allowances.member(disclose(owner))) {
        return 0;
    }

    const spenderAllowances = allowances.lookup(disclose(owner));
    if (!spenderAllowances.member(disclose(spender))) {
        return 0;
    }

    return spenderAllowances.lookup(disclose(spender));
}

// Internal: Set approval
circuit _approve(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): [] {
    // Initialize owner's allowance map if needed
    if (!allowances.member(disclose(owner))) {
        allowances.insert(
            disclose(owner),
            default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>
        );
    }

    // Set allowance for spender
    allowances.lookup(owner).insert(disclose(spender), disclose(amount));
}

// Helper
circuit getBalance(account: Either<ZswapCoinPublicKey, ContractAddress>): Uint<128> {
    if (!balances.member(disclose(account))) {
        return 0;
    }
    return balances.lookup(disclose(account));
}

How It Works

Nested Map Structure

export ledger allowances: Map<
    Either<ZswapCoinPublicKey, ContractAddress>,
    Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>
>;
Allowances use a Map of Maps (nested structure):
  • Outer Map: Owner address → Inner Map
  • Inner Map: Spender address → Allowed amount
Example structure:
Alice → {
  DEX Contract → 1000 tokens,
  Bob → 50 tokens,
  Payment Processor → 100 tokens
}
Bob → {
  Subscription Service → 200 tokens
}
Why nested? Each owner can approve multiple spenders for different amounts. The nested structure tracks all these relationships efficiently.

Granting Approval

export circuit approve(
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): Boolean {
    const owner = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _approve(owner, spender, amount);
    return true;
}
The approval flow:
  1. Owner calls approve(spender, amount)
  2. Owner = ownPublicKey(): Caller is automatically the owner
  3. Permission granted: Spender can now spend up to amount tokens
  4. Returns true: Standard ERC20 return value
Example: Alice calls approve(DEX_CONTRACT, 1000) - the DEX can now spend up to 1000 of Alice’s tokens.
Multiple approvals: You can approve different amounts for different spenders. Each approval is independent.

Setting Approval Internally

circuit _approve(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): [] {
    // Initialize owner's allowance map if needed
    if (!allowances.member(disclose(owner))) {
        allowances.insert(
            disclose(owner),
            default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>
        );
    }

    // Set allowance for spender
    allowances.lookup(owner).insert(disclose(spender), disclose(amount));
}
Two-step process:
  1. Check if owner exists in outer Map, create entry if not
  2. Insert spender amount in owner’s inner Map
This overwrites any previous approval for that spender.

Checking Allowance

export circuit allowance(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
    if (!allowances.member(disclose(owner))) {
        return 0;
    }

    const spenderAllowances = allowances.lookup(disclose(owner));
    if (!spenderAllowances.member(disclose(spender))) {
        return 0;
    }

    return spenderAllowances.lookup(disclose(spender));
}
Two-level lookup:
  1. Check if owner exists in outer Map → return 0 if not
  2. Check if spender exists in owner’s inner Map → return 0 if not
  3. Return amount if both exist
Always check both levels: Forgetting to check either Map membership can cause errors when looking up non-existent keys.

What’s Next