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:
- Owner calls
approve(spender, amount)
- Owner = ownPublicKey(): Caller is automatically the owner
- Permission granted: Spender can now spend up to
amount tokens
- 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:
- Check if owner exists in outer Map, create entry if not
- 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:
- Check if owner exists in outer Map → return 0 if not
- Check if spender exists in owner’s inner Map → return 0 if not
- 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