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>>;

// Transfer tokens from sender to recipient
export circuit transfer(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): Boolean {
    // Get sender (caller of this function)
    const from = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());

    // Perform the transfer
    _transfer(from, to, amount);

    return true;
}

// Internal transfer logic
circuit _transfer(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): [] {
    // Get current balances
    const fromBalance = getBalance(from);
    const toBalance = getBalance(to);

    // Check sufficient balance
    assert(fromBalance >= amount, "Insufficient balance");

    // Check for overflow on receiver
    const MAX_UINT128 = 340282366920938463463374607431768211455 as Uint<128>;
    assert(toBalance <= MAX_UINT128 - amount, "Balance overflow");

    // Update balances atomically
    const newFromBalance = fromBalance - amount;
    const newToBalance = toBalance + amount;

    balances.insert(disclose(from), disclose(newFromBalance));
    balances.insert(disclose(to), disclose(newToBalance));
}

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

How It Works

Authorization with ownPublicKey()

export circuit transfer(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    amount: Uint<128>
): Boolean {
    const from = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _transfer(from, to, amount);
    return true;
}
The transfer() function automatically uses the caller’s identity:
  • ownPublicKey(): Built-in function that returns the public key of whoever called this circuit
  • Implicit authorization: No one can transfer from someone else’s account
  • User calls transfer(recipient, amount): Their own address is automatically the sender
Security: Users never specify “from” in basic transfers. The contract gets it from ownPublicKey(), preventing unauthorized transfers.

Balance Checks

const fromBalance = getBalance(from);
assert(fromBalance >= amount, "Insufficient balance");
Verify the sender has enough tokens:
  • Read first: Get current balance from Map
  • Validate: Ensure sufficient funds
  • Clear error: Tell user why transfer failed

Overflow Protection

const toBalance = getBalance(to);
const MAX_UINT128 = 340282366920938463463374607431768211455 as Uint<128>;
assert(toBalance <= MAX_UINT128 - amount, "Balance overflow");
Prevent integer overflow on the receiver - ensure the addition won’t wrap around to 0. See Overflow Protection for detailed arithmetic safety patterns.

Atomic Updates

const newFromBalance = fromBalance - amount;
const newToBalance = toBalance + amount;

balances.insert(disclose(from), disclose(newFromBalance));
balances.insert(disclose(to), disclose(newToBalance));
Update both balances in sequence:
  • Calculate first: Compute new values before writing
  • Write together: Both updates happen in same transaction
  • All-or-nothing: If one fails, entire transaction reverts
The transfer is atomic - either both accounts update or neither does.

What’s Next