pragma language_version >= 0.23;
import CompactStandardLibrary;
// Caller keypair (witness-derived) — used to authenticate users
struct UserSecretKey { bytes: Bytes<32>; }
struct UserPublicKey { bytes: Bytes<32>; }
witness getUserSecret(): UserSecretKey;
// Admin keypair (witness-derived) — gates mint and burn
struct AdminSecretKey { bytes: Bytes<32>; }
struct AdminPublicKey { bytes: Bytes<32>; }
witness getAdminSecret(): AdminSecretKey;
// Token metadata (sealed: settable only in the constructor)
export sealed ledger _name: Opaque<"string">;
export sealed ledger _symbol: Opaque<"string">;
export sealed ledger _decimals: Uint<8>;
// On-chain admin and ledger state
export ledger _admin: AdminPublicKey;
export ledger _balances: Map<UserPublicKey, Uint<128>>;
export ledger _allowances: Map<UserPublicKey, Map<UserPublicKey, Uint<128>>>;
export ledger _totalSupply: Uint<128>;
constructor(
initialAdmin: AdminPublicKey,
name_: Opaque<"string">,
symbol_: Opaque<"string">,
decimals_: Uint<8>
) {
_admin = disclose(initialAdmin);
_name = disclose(name_);
_symbol = disclose(symbol_);
_decimals = disclose(decimals_);
}
// Key derivation
export circuit deriveUserPublicKey(sk: UserSecretKey): UserPublicKey {
return UserPublicKey {
bytes: persistentHash<Vector<2, Bytes<32>>>([
pad(32, "erc20:user:v1"),
sk.bytes
])
};
}
export circuit deriveAdminPublicKey(sk: AdminSecretKey): AdminPublicKey {
return AdminPublicKey {
bytes: persistentHash<Vector<2, Bytes<32>>>([
pad(32, "erc20:admin:v1"),
sk.bytes
])
};
}
// Metadata readers
export circuit name(): Opaque<"string"> { return _name; }
export circuit symbol(): Opaque<"string"> { return _symbol; }
export circuit decimals(): Uint<8> { return _decimals; }
export circuit totalSupply(): Uint<128> { return _totalSupply; }
export circuit balanceOf(account: UserPublicKey): Uint<128> {
if (!_balances.member(disclose(account))) { return 0; }
return _balances.lookup(disclose(account));
}
// Transfer — caller authenticates as the sender
export circuit transfer(to: UserPublicKey, value: Uint<128>): Boolean {
const owner = disclose(deriveUserPublicKey(getUserSecret()));
_transfer(owner, to, value);
return true;
}
// Approve — caller authenticates as the owner
export circuit approve(spender: UserPublicKey, value: Uint<128>): Boolean {
const owner = disclose(deriveUserPublicKey(getUserSecret()));
_approve(owner, spender, value);
return true;
}
// Read remaining allowance
export circuit allowance(owner: UserPublicKey, spender: UserPublicKey): Uint<128> {
if (!_allowances.member(disclose(owner))) { return 0; }
if (!_allowances.lookup(disclose(owner)).member(disclose(spender))) { return 0; }
return _allowances.lookup(disclose(owner)).lookup(disclose(spender));
}
// transferFrom — caller authenticates as the spender
export circuit transferFrom(
owner: UserPublicKey,
to: UserPublicKey,
value: Uint<128>
): Boolean {
const spender = disclose(deriveUserPublicKey(getUserSecret()));
_spendAllowance(owner, spender, value);
_transfer(owner, to, value);
return true;
}
// Admin: mint
export circuit mint(account: UserPublicKey, value: Uint<128>): [] {
assertAdmin();
_mint(account, value);
}
// Admin: burn (forceful)
export circuit adminBurn(account: UserPublicKey, value: Uint<128>): [] {
assertAdmin();
_burn(account, value);
}
// Self-burn: caller burns their own tokens
export circuit burn(value: Uint<128>): [] {
const account = disclose(deriveUserPublicKey(getUserSecret()));
_burn(account, value);
}
// Internals
circuit assertAdmin(): [] {
assert(_admin == deriveAdminPublicKey(getAdminSecret()), "Not admin");
}
circuit _transfer(
sender: UserPublicKey,
to: UserPublicKey,
value: Uint<128>
): [] {
const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
const fromBal = balanceOf(sender);
assert(fromBal >= value, "Insufficient balance");
const toBal = balanceOf(to);
assert(toBal <= MAX_UINT128 - value, "Balance overflow");
_balances.insert(disclose(sender), disclose(fromBal - value));
_balances.insert(disclose(to), disclose((toBal + value) as Uint<128>));
}
circuit _approve(
owner: UserPublicKey,
spender: UserPublicKey,
value: Uint<128>
): [] {
if (!_allowances.member(disclose(owner))) {
_allowances.insert(disclose(owner), default<Map<UserPublicKey, Uint<128>>>);
}
_allowances.lookup(disclose(owner)).insert(disclose(spender), disclose(value));
}
circuit _spendAllowance(
owner: UserPublicKey,
spender: UserPublicKey,
value: Uint<128>
): [] {
const current = allowance(owner, spender);
const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
if (current < MAX_UINT128) {
assert(current >= value, "Insufficient allowance");
_approve(owner, spender, current - value);
}
}
circuit _mint(account: UserPublicKey, value: Uint<128>): [] {
const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
assert(_totalSupply <= MAX_UINT128 - value, "Total supply overflow");
_totalSupply = disclose((_totalSupply + value) as Uint<128>);
const bal = balanceOf(account);
assert(bal <= MAX_UINT128 - value, "Balance overflow");
_balances.insert(disclose(account), disclose((bal + value) as Uint<128>));
}
circuit _burn(account: UserPublicKey, value: Uint<128>): [] {
const bal = balanceOf(account);
assert(bal >= value, "Insufficient balance");
assert(_totalSupply >= value, "Total supply underflow");
_balances.insert(disclose(account), disclose(bal - value));
_totalSupply = disclose(_totalSupply - value);
}