From 9223ab61fb7e799a202cef9d1c6626943c2478bb Mon Sep 17 00:00:00 2001 From: Hitakshi Arora Date: Sun, 12 Jul 2026 10:06:33 +0530 Subject: [PATCH] fix(wallet): decrypt the private balance with the SDK's own eERC key The eERC SDK's register() ignores the decryptionKey passed to the EERC constructor and derives its own from a deterministic wallet signature (generateDecryptionKey), binding the on-chain public key and the ElGamal-encrypted balance to THAT key. Our read path constructed a fresh EERC with account.eercDecryptionKey and never called generateDecryptionKey, so calculateTotalBalance decrypted with the wrong key, failed its eGCT verify, and every shielded balance rendered $0.00 even though the deposit was on-chain (activity showed it correctly). Have createEerc install the SDK's own key up front. The signature is local (self-custody key) and deterministic, so it is stable across sessions and matches whatever key registration used. Verified end-to-end on Fuji: a fresh wallet that deposited 0.04 USDC now decrypts to 40000 (was 0). --- apps/wallet/src/lib/eerc.shield.test.ts | 3 +++ apps/wallet/src/lib/eerc.ts | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/wallet/src/lib/eerc.shield.test.ts b/apps/wallet/src/lib/eerc.shield.test.ts index 34f7d7e..a4a6e89 100644 --- a/apps/wallet/src/lib/eerc.shield.test.ts +++ b/apps/wallet/src/lib/eerc.shield.test.ts @@ -19,6 +19,7 @@ const eercMocks = vi.hoisted(() => { const withdraw = vi.fn(); const transfer = vi.fn(); const calculateTotalBalance = vi.fn(); + const generateDecryptionKey = vi.fn(async () => `0x${"a".repeat(64)}`); class MockEERC { client = publicClient; @@ -29,6 +30,7 @@ const eercMocks = vi.hoisted(() => { withdraw = withdraw; transfer = transfer; calculateTotalBalance = calculateTotalBalance; + generateDecryptionKey = generateDecryptionKey; } return { @@ -36,6 +38,7 @@ const eercMocks = vi.hoisted(() => { calculateTotalBalance, deposit, fetchPublicKey, + generateDecryptionKey, publicClient, register, transfer, diff --git a/apps/wallet/src/lib/eerc.ts b/apps/wallet/src/lib/eerc.ts index aaea8cc..feeacfa 100644 --- a/apps/wallet/src/lib/eerc.ts +++ b/apps/wallet/src/lib/eerc.ts @@ -123,7 +123,7 @@ export async function createEerc(account: BenzoAccount): Promise { if (!ENCRYPTED_ERC_ADDRESS || !REGISTRAR_ADDRESS) return null; const EERCClass = await loadEERC(); const { publicClient, walletClient } = createViemClients(account); - return new EERCClass( + const eerc = new EERCClass( publicClient, walletClient, ENCRYPTED_ERC_ADDRESS, @@ -132,6 +132,17 @@ export async function createEerc(account: BenzoAccount): Promise { EERC_CIRCUIT_URLS, account.eercDecryptionKey, ); + // The eERC SDK does NOT use the `decryptionKey` we pass to the constructor for + // anything that touches chain state: register() derives its OWN key via + // generateDecryptionKey() (a deterministic wallet signature) and binds the + // on-chain public key + ElGamal-encrypted balance to THAT key. If we then + // decrypt with our own `account.eercDecryptionKey`, calculateTotalBalance()'s + // eGCT verify fails and every private balance reads $0. So let the SDK derive + // and install its own key (and matching publicKey) up front — the signature is + // local (self-custody key) and deterministic, so it's stable across sessions + // and matches whatever key registration used. + await eerc.generateDecryptionKey(); + return eerc; } function eercPublicClient(eerc: EERC): PublicClient {