Confidential Transfer
The ConfidentialTransfer contract manages encrypted balances using ElGamal encryption on the STARK curve. Every balance is stored as a ciphertext — the contract never sees plaintext amounts. Six zero-knowledge proofs verify each transfer on-chain.
How It Works
Six Proofs Per Transfer
Every confidential transfer requires six zero-knowledge proofs, all verified on-chain:
| # | Proof | What It Proves |
|---|---|---|
| 1 | Ownership (Schnorr) | Sender knows their private key |
| 2 | Blinding Proof | Encryption randomness is correctly formed |
| 3 | Encryption (Chaum-Pedersen) | Ciphertext correctly encrypts the amount under recipient's key |
| 4 | Range Proof | Amount is in valid range (0 to 2^32), preventing overflow |
| 5 | Same-Encryption (sender/receiver) | Both ciphertexts encrypt the same amount |
| 6 | Same-Encryption (receiver/auditor) | Auditor ciphertext matches (for compliance) |
Because ElGamal is additively homomorphic, the contract updates balances by adding ciphertexts: new_balance = old_balance ⊕ Enc(delta). The contract never decrypts — it just manipulates ciphertexts.
Balance Model
Each user's balance = ElGamal ciphertext (L, R)
L = amount · G + randomness · pk
R = randomness · G
To add delta:
L' = L + delta·G + r'·pk
R' = R + r'·G
Net effect: encrypted balance increases by delta
Pending Transfers
As an anti-spam mechanism, incoming transfers are placed in a pending state. The recipient must call rollover() to accept pending transfers into their active balance.
// Accept pending transfers
await obelysk.confidentialTransfer.rollover('sage');
AE Hints for Fast Decryption
Decrypting ElGamal normally requires solving discrete log (expensive). Instead, the sender includes an AE hint — the plaintext amount encrypted with a shared ECDH secret:
shared_secret = ECDH(sender_sk, recipient_pk)
ae_hint = AES_Encrypt(shared_secret, amount)
The recipient decrypts the hint in O(1) instead of brute-forcing discrete log.
Auditor Keys
The contract supports an optional auditor escrow key. When set, every transfer also encrypts the amount under the auditor's public key (proven by the 6th proof). This enables compliance monitoring without breaking privacy for regular observers.
The auditor key is optional and configurable by the contract owner. When disabled, only sender and recipient can see the amount. When enabled, a designated auditor can also decrypt for compliance purposes.
SDK Usage
// Register your ElGamal public key
await obelysk.confidentialTransfer.register(myPublicKey);
// Fund encrypted balance (public → private)
await obelysk.confidentialTransfer.fund({
token: 'sage',
amount: '5000',
});
// Transfer (private → private, 6 proofs generated automatically)
await obelysk.confidentialTransfer.transfer({
token: 'sage',
amount: '1000',
recipient: '0xRECIPIENT',
});
// Withdraw (private → public)
await obelysk.confidentialTransfer.withdraw({
token: 'sage',
amount: '2000',
});
// Check encrypted balance
const balance = await obelysk.confidentialTransfer.getEncryptedBalance(
'0xADDRESS', 'sage'
);
Contract Details
- Address:
0x0673685bdb01fbf57c390ec2c0d893e7c77316cdea315b0fbfbc85b9a9a979d2 - Encryption: ElGamal over STARK curve
- Proofs: 6 Schnorr/Chaum-Pedersen per transfer
- Upgrade timelock: 5 minutes (fast upgrade for security patches)
Next Steps
- ElGamal Encryption — full encryption scheme details
- STARK Curve Primitives — Schnorr and Pedersen internals
- Privacy Pools — alternative shielding via Merkle trees