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>>
>;
// 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));
}
// Transfer from another account (must be approved)
export circuit transferFrom(
from: Either<ZswapCoinPublicKey, ContractAddress>,
to: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): Boolean {
const spender = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
// Check and spend allowance
_spendAllowance(from, spender, amount);
// Perform transfer
_transfer(from, to, amount);
return true;
}
// Internal: Spend allowance
circuit _spendAllowance(
owner: Either<ZswapCoinPublicKey, ContractAddress>,
spender: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
const currentAllowance = allowance(owner, spender);
// Check for infinite allowance
const MAX_UINT128 = 340282366920938463463374607431768211455 as Uint<128>;
if (currentAllowance == MAX_UINT128) {
return; // Infinite approval, don't decrease
}
// Check sufficient allowance
assert(currentAllowance >= amount, "Insufficient allowance");
// Decrease allowance
_approve(owner, spender, currentAllowance - amount);
}
// Internal: Set approval
circuit _approve(
owner: Either<ZswapCoinPublicKey, ContractAddress>,
spender: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
if (!allowances.member(disclose(owner))) {
allowances.insert(
disclose(owner),
default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>
);
}
allowances.lookup(owner).insert(disclose(spender), disclose(amount));
}
// Internal: Transfer tokens
circuit _transfer(
from: Either<ZswapCoinPublicKey, ContractAddress>,
to: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
const fromBalance = getBalance(from);
assert(fromBalance >= amount, "Insufficient balance");
const toBalance = getBalance(to);
const MAX_UINT128 = 340282366920938463463374607431768211455 as Uint<128>;
assert(toBalance <= MAX_UINT128 - amount, "Balance overflow");
balances.insert(disclose(from), disclose(fromBalance - amount));
balances.insert(disclose(to), disclose(toBalance + 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
TransferFrom Flow
export circuit transferFrom(
from: Either<ZswapCoinPublicKey, ContractAddress>,
to: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): Boolean {
const spender = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
// Check and spend allowance
_spendAllowance(from, spender, amount);
// Perform transfer
_transfer(from, to, amount);
return true;
}
The transferFrom pattern:
- Spender calls
transferFrom(owner, recipient, amount)
- Spender = ownPublicKey(): Caller must be the approved spender
- Verify allowance: Check spender is approved for at least
amount
- Decrease allowance: Spend the approved tokens
- Transfer tokens: Move from owner to recipient
Example: DEX calls transferFrom(alice, buyer, 500) to sell Alice’s tokens. This works because Alice previously called approve(DEX, 1000).
Three parties involved: Owner (has tokens), Spender (approved to move
them), Recipient (receives tokens). Spender and recipient can be different!
Spending Allowance
circuit _spendAllowance(
owner: Either<ZswapCoinPublicKey, ContractAddress>,
spender: Either<ZswapCoinPublicKey, ContractAddress>,
amount: Uint<128>
): [] {
const currentAllowance = allowance(owner, spender);
// Check for infinite allowance
const MAX_UINT128 = 340282366920938463463374607431768211455 as Uint<128>;
if (currentAllowance == MAX_UINT128) {
return; // Infinite approval, don't decrease
}
// Check sufficient allowance
assert(currentAllowance >= amount, "Insufficient allowance");
// Decrease allowance
_approve(owner, spender, currentAllowance - amount);
}
Allowance spending logic:
- Get current allowance: Check how much spender can spend
- Infinite approval check: If allowance = MAX_UINT128, skip decrease (infinite approval)
- Sufficient check: Verify spender has enough allowance
- Decrease allowance: Subtract spent amount from remaining allowance
See Overflow Protection for safe arithmetic patterns.
Checking Remaining 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));
}
Anyone can check how much a spender is allowed to spend:
-
Public visibility for transparency
-
Used by front-ends to show approval status
-
Returns 0 if no approval exists
-
Returns 0 if no approval exists
Partial Spending
// Alice approves Bob for 1000 tokens
_approve(alice, bob, 1000);
// Bob can spend in multiple transactions
_spendAllowance(alice, bob, 300); // Remaining: 700
_transfer(alice, recipient1, 300);
_spendAllowance(alice, bob, 400); // Remaining: 300
_transfer(alice, recipient2, 400);
_spendAllowance(alice, bob, 300); // Remaining: 0
_transfer(alice, recipient3, 300);
// Bob's allowance is now exhausted
Allowances can be spent gradually over multiple transactions.
What’s Next