diff --git a/SNIPS/snip-9999.md b/SNIPS/snip-9999.md new file mode 100644 index 0000000..af08164 --- /dev/null +++ b/SNIPS/snip-9999.md @@ -0,0 +1,523 @@ +--- +snip: +title: Zero-Knowledge Token Wrapper +description: Interface for adding privacy to existing Starknet tokens using zero-knowledge proofs +author: zyrav21 (@zyrav21) +discussions-to: https://github.com/starknet-io/SNIPs/pull/TBD +status: Draft +type: Standards Track +category: SRC +created: 2025-11-05 +requires: 2 +--- + +## Simple Summary + +A permissionless standard for wrapping any Starknet token with zero-knowledge privacy features, enabling private transactions while maintaining full exchange compatibility. + +## Abstract + +This SNIP defines a standard for Zero-Knowledge Wrapper Tokens (ZKWTokens) that enable private transactions for existing Starknet tokens without modifying the underlying token contracts. The standard leverages STARK proofs, Pedersen commitments, and nullifier-based private notes to achieve privacy while maintaining full SNIP-2 (ERC20) compatibility for exchange integration and DeFi composability. Unlike Ethereum's EIP-7503 burn-and-remint approach, this standard is optimized for Starknet's account abstraction, native STARK proving, and low L2 transaction costs. + +## Motivation + +Most existing tokens on Starknet lack native privacy features. Users seeking privacy must rely on dedicated privacy protocols, which limit token usability, reduce composability, and fragment liquidity. This SNIP addresses these limitations by introducing a permissionless privacy wrapper that: + +1. **Pluggable Privacy**: Preserves all properties of the underlying token while adding zero-knowledge privacy +2. **Permissionless**: Any user can wrap any SNIP-2 token or ETH without issuer permission +3. **L2-Optimized**: Designed for Starknet's low transaction costs and native STARK verification +4. **Account Abstraction Native**: Leverages Starknet's native AA for enhanced privacy patterns +5. **Dual-Mode Operation**: Supports both public (exchange-compatible) and private (ZK-note) states +6. **STARK-Based**: Uses transparent, quantum-resistant STWO Circle STARKs + +### Key Differences from Ethereum EIP-7503 + +| Feature | EIP-7503 (Ethereum L1) | SNIP-zkWrapper (Starknet L2) | +|---------|------------------------|------------------------------| +| **Proof System** | Generic ZK (expensive) | Native STARK (efficient) | +| **Cost Model** | High L1 gas costs | Low L2 costs (~$0.01-0.10) | +| **Account Model** | EOA-based | Account Abstraction native | +| **Privacy Model** | Burn-and-remint | UTXO-style private notes | +| **Commitment Scheme** | Contract-managed Merkle | Pedersen + STARK commitments | +| **Nullifier Management** | On-chain verification | Component-based verification | + +## Specification + +The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +### Overview + +A Zero-Knowledge Wrapper Token (ZKWToken) provides the following core functionalities: + +1. **Deposit**: Wraps an existing token (ETH or SNIP-2) and mints the corresponding amount of ZKWToken +2. **Shield**: Converts public ZKWToken balance to private note (commitment-based) +3. **Transfer Private Note**: Transfers private notes with hidden amounts using ZK proofs +4. **Burn Private Note**: Converts private note back to public ZKWToken +5. **Withdraw**: Burns ZKWToken to redeem the underlying token + +### Core Interface + +```cairo +#[starknet::interface] +pub trait IZKWrapper { + // ═══════════════════════════════════════════════════════════ + // WRAPPER FUNCTIONS - Deposit/Withdraw + // ═══════════════════════════════════════════════════════════ + + /// Deposit underlying token and mint ZKWToken 1:1 + /// @param amount The amount to deposit (in token's decimals) + /// @return Success boolean + fn deposit(ref self: TContractState, amount: u256) -> bool; + + /// Withdraw underlying token by burning ZKWToken 1:1 + /// @param amount The amount to withdraw (in token's decimals) + /// @return Success boolean + fn withdraw(ref self: TContractState, amount: u256) -> bool; + + /// Get total collateral locked in wrapper + /// @return Total underlying token locked + fn get_collateral(self: @TContractState) -> u256; + + /// Get underlying token contract address + /// @return ContractAddress of wrapped token + fn get_underlying(self: @TContractState) -> ContractAddress; + + // ═══════════════════════════════════════════════════════════ + // SNIP-2 (ERC20) STANDARD FUNCTIONS - Exchange Compatibility + // ═══════════════════════════════════════════════════════════ + + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn transfer(ref self: TContractState, to: ContractAddress, amount: u256) -> bool; + fn transfer_from(ref self: TContractState, from: ContractAddress, to: ContractAddress, amount: u256) -> bool; + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn total_supply(self: @TContractState) -> u256; + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn decimals(self: @TContractState) -> u8; + + // CamelCase compatibility + fn balanceOf(self: @TContractState, account: ContractAddress) -> u256; + fn transferFrom(ref self: TContractState, from: ContractAddress, to: ContractAddress, amount: u256) -> bool; + fn totalSupply(self: @TContractState) -> u256; + + // ═══════════════════════════════════════════════════════════ + // PRIVACY BRIDGE FUNCTIONS - Shield/Unshield + // ═══════════════════════════════════════════════════════════ + + /// Convert public ZKWToken to private note + /// @param amount Amount to shield + /// @param recipient_commitment Pedersen commitment for recipient + /// @return Success boolean + fn shield_tokens(ref self: TContractState, amount: u256, recipient_commitment: felt252) -> bool; + + // ═══════════════════════════════════════════════════════════ + // ZK-NATIVE OPERATIONS - Maximum Privacy + // ═══════════════════════════════════════════════════════════ + + /// Transfer private notes with hidden amounts + /// @param input_nullifiers Array of input note nullifiers (prevent double-spend) + /// @param output_commitments Array of output note commitments (new private notes) + /// @param proof_hash Hash of off-chain STARK proof + /// @param proof_storage_uri IPFS/Arweave URI for proof data + /// @param nullifier_proofs Proofs that nullifiers are valid + /// @param encrypted_metadata Encrypted transaction metadata + /// @return Success boolean + fn transfer_private_note( + ref self: TContractState, + input_nullifiers: Array, + output_commitments: Array, + proof_hash: felt252, + proof_storage_uri: ByteArray, + nullifier_proofs: Array, + encrypted_metadata: Array + ) -> bool; + + /// Register off-chain proof hash for later verification + /// @param proof_hash Hash of STARK proof + /// @param storage_uri URI where full proof is stored + /// @return Success boolean + fn register_proof_hash( + ref self: TContractState, + proof_hash: felt252, + storage_uri: ByteArray + ) -> bool; + + /// Burn private note and mint public ZKWToken to recipient + /// @param nullifier Note nullifier (prevents double-spend) + /// @param amount Amount to unshield + /// @param recipient Recipient of public tokens + /// @param proof_hash Hash of ownership proof + /// @param proof_storage_uri URI of proof data + /// @return Success boolean + fn burn_private_note( + ref self: TContractState, + nullifier: felt252, + amount: u256, + recipient: ContractAddress, + proof_hash: felt252, + proof_storage_uri: ByteArray + ) -> bool; + + // ═══════════════════════════════════════════════════════════ + // PRIVACY STATISTICS - Public Queries + // ═══════════════════════════════════════════════════════════ + + /// Get total number of private notes created + fn get_total_notes_created(self: @TContractState) -> u64; + + /// Check if nullifier has been used (prevents double-spend) + fn is_nullifier_used(self: @TContractState, nullifier: felt252) -> bool; + + /// Check if commitment is valid (note exists) + fn is_commitment_valid(self: @TContractState, commitment: felt252) -> bool; +} +``` + +### Events + +```cairo +#[derive(Drop, starknet::Event)] +struct Deposited { + pub user: ContractAddress, + pub underlying_amount: u256, + pub zkwtoken_minted: u256, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +struct Withdrawn { + pub user: ContractAddress, + pub zkwtoken_burned: u256, + pub underlying_returned: u256, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +struct TokensShielded { + pub commitment_hash: felt252, // Hash of commitment (not the commitment itself) + pub shield_timestamp: u64, + pub operation_index: u64, +} + +#[derive(Drop, starknet::Event)] +struct TokensUnshielded { + pub nullifier_hash: felt252, // Hash of nullifier (not the nullifier itself) + pub unshield_timestamp: u64, + pub operation_index: u64, +} + +#[derive(Drop, starknet::Event)] +struct PrivateNoteTransferred { + pub input_nullifiers_hash: felt252, // Hash of all input nullifiers + pub output_commitments_hash: felt252, // Hash of all output commitments + pub transaction_hash: felt252, // Proof hash for verification +} + +#[derive(Drop, starknet::Event)] +struct PrivateNoteBurned { + pub nullifier: felt252, // Nullifier revealed only on unshield + pub recipient: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +struct ProofRegistered { + pub proof_hash: felt252, + pub storage_uri: ByteArray, + pub timestamp: u64, + pub registered_by: ContractAddress, +} +``` + +### Behavioral Specification + +#### Deposit Flow + +1. **MUST** validate `amount > 0` +2. **MUST** check user has sufficient underlying token balance +3. **MUST** check contract has sufficient allowance +4. **MUST** use Checks-Effects-Interactions (CEI) pattern: + - Update wrapper state (collateral, balances, supply) + - Transfer underlying token from user (external call last) +5. **MUST** maintain strict 1:1 collateralization ratio +6. **MUST** emit `Transfer` (from zero address) and `Deposited` events + +#### Shield Flow + +1. **MUST** validate `amount > 0` +2. **MUST** check user has sufficient public ZKWToken balance +3. **MUST** burn public ZKWToken from user +4. **MUST** store `recipient_commitment` in commitments map +5. **MUST** increment private supply counter +6. **MUST** emit `Transfer` (to zero address) and `TokensShielded` events +7. **MUST** use Pedersen hash for commitment derivation + +#### Transfer Private Note Flow + +1. **MUST** verify or register proof hash +2. **MUST** validate all input nullifiers are unused +3. **MUST** mark all input nullifiers as spent +4. **MUST** store all output commitments +5. **MUST** validate `input_nullifiers.len() > 0` and `output_commitments.len() > 0` +6. **MUST** emit `PrivateNoteTransferred` event with hashes only (no amounts) +7. **SHOULD** support off-chain STARK proof verification + +#### Burn Private Note Flow + +1. **MUST** verify or register proof hash +2. **MUST** validate nullifier is unused +3. **MUST** mark nullifier as spent +4. **MUST** mint public ZKWToken to recipient +5. **MUST** update supplies (decrease private, increase public) +6. **MUST** emit `Transfer` (from zero address) and `PrivateNoteBurned` events + +#### Withdraw Flow + +1. **MUST** validate `amount > 0` +2. **MUST** check user has sufficient public ZKWToken balance +3. **MUST** check wrapper has sufficient underlying collateral +4. **MUST** use CEI pattern: + - Burn ZKWToken from user + - Update collateral counter + - Transfer underlying token to user (external call last) +5. **MUST** maintain 1:1 collateralization +6. **MUST** emit `Transfer` (to zero address) and `Withdrawn` events + +### Security Requirements + +#### Collateralization + +- **MUST** maintain strict 1:1 ratio between ZKWToken supply and underlying collateral +- **MUST NOT** allow withdrawals exceeding locked collateral +- **SHOULD** implement emergency pause mechanism + +#### Privacy Guarantees + +- **MUST** use Pedersen commitments for note privacy +- **MUST** use STARK proofs for range proofs and amount validity +- **MUST** never expose amounts in private transfer events +- **MUST** never expose addresses in private transfer events +- **MUST** use nullifiers to prevent double-spending +- **SHOULD** use off-chain proof storage to minimize gas costs + +#### Reentrancy Protection + +- **MUST** follow CEI pattern in all external calls +- **SHOULD** consider reentrancy guard for additional safety +- **MUST** update state before external token transfers + +#### Access Control + +- **SHOULD** implement admin role for emergency functions +- **SHOULD** implement pausable functionality +- **MAY** implement multi-sig governance + +### Privacy Features by Token Type + +#### Fungible Tokens (ETH, SNIP-2) + +- **CAN** break traceability through shield → private transfer → unshield flows +- **CAN** hide holder identity using commitments and nullifiers +- **CAN** hide amounts in private transfers using STARK range proofs +- **ENABLES** anonymous mixing through private note transfers + +#### Non-Fungible Tokens (Future Extension) + +- **CANNOT** break traceability (each NFT is unique) +- **CAN** hide holder identity until withdrawal +- **MAY** use commitment-based ownership tracking + +## Rationale + +### Why Starknet-Specific Standard? + +1. **Native STARK Support**: Starknet's native STARK verification makes ZK proof validation efficient and cost-effective +2. **Account Abstraction**: Enables advanced privacy patterns (e.g., ephemeral accounts, multi-sig privacy) +3. **Low L2 Costs**: Makes privacy economically viable for everyday transactions ($0.01-0.10 vs $5-50 on L1) +4. **Cairo Type System**: Strong typing and felt252 enable secure cryptographic primitives + +### Design Decisions + +#### Off-Chain Proof Storage + +**Decision**: Store proof data off-chain (IPFS/Arweave), only hash on-chain + +**Rationale**: +- STARK proofs are large (~2-10KB) +- On-chain storage is expensive even on L2 +- Proof hash provides sufficient verification +- Users can fetch proofs from decentralized storage + +#### UTXO-Style Private Notes vs Burn-and-Remint + +**Decision**: Use UTXO-style private notes with nullifiers + +**Rationale**: +- More flexible than burn-and-remint (supports multi-input/output) +- Better anonymity set (notes unlinkable to amounts) +- Enables private note splitting and combining +- Compatible with existing privacy research (Zcash, Monero) + +#### Dual-Mode (Public + Private) + +**Decision**: Support both public ERC20 and private note modes + +**Rationale**: +- Exchange compatibility requires public balances +- Privacy requires private notes +- Users can choose based on use case +- Enables gradual privacy adoption + +#### Pedersen Commitments + +**Decision**: Use Pedersen commitments for note commitments + +**Rationale**: +- Efficient in Cairo (native Pedersen builtin) +- Cryptographically secure (binding + hiding) +- Compatible with STARK proving system +- Industry-standard for ZK applications + +## Backwards Compatibility + +This SNIP is fully compatible with: + +- **SNIP-2 (ERC20)**: ZKWToken implements full ERC20 interface +- **Existing DEXs**: Public mode works with AMMs and order books +- **Wallet Infrastructure**: Standard transfer/approve functions +- **Block Explorers**: Public transactions visible, private transactions show hashes + +**Breaking Changes**: None (additive standard) + +## Test Cases + +### Test Case 1: Deposit and Withdraw + +```cairo +// Setup +let underlying_token = deploy_erc20("ETH", "ETH", 18); +let zkwrapper = deploy_zkwrapper(underlying_token.contract_address); + +// Test deposit +underlying_token.approve(zkwrapper.contract_address, 1000); +assert(zkwrapper.deposit(1000) == true); +assert(zkwrapper.balance_of(user) == 1000); +assert(zkwrapper.get_collateral() == 1000); + +// Test withdraw +assert(zkwrapper.withdraw(500) == true); +assert(zkwrapper.balance_of(user) == 500); +assert(zkwrapper.get_collateral() == 500); +assert(underlying_token.balance_of(user) == 500); +``` + +### Test Case 2: Shield and Private Transfer + +```cairo +// Setup +zkwrapper.deposit(1000); + +// Generate commitment +let commitment = pedersen(amount, random_blinding_factor); + +// Shield tokens +assert(zkwrapper.shield_tokens(1000, commitment) == true); +assert(zkwrapper.balance_of(user) == 0); +assert(zkwrapper.is_commitment_valid(commitment) == true); + +// Private transfer +let proof = generate_stark_proof(inputs, outputs); +let proof_hash = hash_proof(proof); +let nullifier = derive_nullifier(commitment, nullifier_seed); + +assert(zkwrapper.transfer_private_note( + array![nullifier], + array![new_commitment], + proof_hash, + "ipfs://...", + array![nullifier_proof], + array![] +) == true); + +assert(zkwrapper.is_nullifier_used(nullifier) == true); +assert(zkwrapper.is_commitment_valid(new_commitment) == true); +``` + +### Test Case 3: Collateralization Invariant + +```cairo +// Invariant: public_supply + private_supply == collateral +let public_supply = zkwrapper.total_supply(); +let collateral = zkwrapper.get_collateral(); + +// After any operation: +assert(public_supply <= collateral); +// Note: private_supply is not exposed but internally tracked +``` + +## Implementation + +A complete reference implementation is available at: +- **Contract**: `/contracts/src/tokens/zketh_wrapper.cairo` +- **Components**: + - `/contracts/src/components/stwo_verification_component.cairo` + - `/contracts/src/components/pedersen_component.cairo` + - `/contracts/src/components/nullifier_component.cairo` +- **Demo**: Live testnet deployment (contact author for URL) + +**Note**: Reference implementation uses STWO Circle STARKs for proof verification. Implementers MAY use alternative STARK proof systems as long as security properties are maintained. + +## Security Considerations + +### Cryptographic Security + +1. **Nullifier Uniqueness**: Nullifiers MUST be cryptographically unique to prevent double-spending +2. **Commitment Hiding**: Pedersen commitments MUST be computationally hiding +3. **Commitment Binding**: Pedersen commitments MUST be computationally binding +4. **Range Proofs**: Amounts MUST be proven to be within valid range (0 to max_supply) +5. **Proof Verification**: STARK proofs MUST be verified before accepting transactions + +### Privacy Leakage Risks + +1. **Amount Correlation**: Shield/unshield operations reveal amounts (bridge operations) + - **Mitigation**: Use batching or delayed commitment revelation +2. **Timing Analysis**: Transaction timing can reveal patterns + - **Mitigation**: Use batched operations or time-delayed execution +3. **Network Analysis**: IP addresses can be correlated + - **Mitigation**: Recommend users use Tor/VPN +4. **Address Reuse**: Burning to same address reduces anonymity + - **Mitigation**: Use stealth addresses or one-time addresses + +### Economic Security + +1. **Collateral Audit**: Users SHOULD verify collateral ratio regularly +2. **Oracle Independence**: Design MUST NOT rely on price oracles (avoids manipulation) +3. **Emergency Withdrawals**: Implement pausable mechanism for critical bugs +4. **Upgrade Security**: Use multi-sig or DAO for contract upgrades + +### Implementation Pitfalls + +1. **Integer Overflow**: Use Cairo's safe math (automatic in Cairo 1.0+) +2. **Reentrancy**: Follow CEI pattern strictly +3. **Gas Griefing**: Validate array sizes to prevent DoS +4. **Front-Running**: Private transactions are resistant but shield/unshield can be front-run +5. **Proof Malleability**: Ensure proofs are non-malleable + +### Quantum Resistance + +- **STARK Proofs**: Quantum-resistant (hash-based) +- **Pedersen Commitments**: Post-quantum migration possible to hash-based commitments +- **Overall Assessment**: More quantum-resistant than zk-SNARKs + +### Regulatory Considerations + +- **Permissionless**: No KYC/AML built into protocol +- **Transparency**: On-chain data available for legitimate investigations +- **User Responsibility**: Users responsible for legal compliance +- **Auditability**: Proof data stored off-chain enables external audits + +## Copyright + +Copyright and related rights waived via [MIT](../LICENSE). +