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