Introduction

In the two weeks from September 22nd to October 6th 2023, zkSecurity performed a security audit of Silent Protocol’s Circom circuits.

A number of observations and findings have been reported to the Silent Protocol team. The findings are detailed in the latter section of this report.

Silent Protocol’s circuits were found to be of high quality, accompanied with thorough documentation, specifications and tests. As of writing, all high and medium-severity issues we found were patched by the Silent Protocol team, and zkSecurity confirms that these patches properly address our findings.

Note that security audits are a valuable tool for identifying and mitigating security risks, but they are not a guarantee of perfect security. Security is a continuous process, and organizations should always be working to improve their security posture.

Scope

A consultant from zkSecurity spent two weeks auditing the Circom circuits for the Silent multi-asset shielded pool (SMASP) application. These circuits represent the privacy-preserving portion of the overall SMASP application. They are used in a set of Solidity smart contracts that verify zero-knowledge proofs created from these circuits.

Smart contracts were reviewed by zkSecurity at an earlier date, and were only in scope insofar as they perform preparation and validation of public circuit inputs. The main focus of this audit were the application’s circuits written in Circom. These include circuits for the main SMASP protocol, circuits used by the compliance logic, and circuits representing future functionality not used in the contracts at the time of review. An overview of audited circuits can be found below.

The audit also covers all auxiliary circuit templates and utilities maintained by Silent Protocol itself, including (but not limited to) templates for foreign-field arithmetic and ElGamal encryption. Not covered by the audit are templates imported from circomlib, Circom’s standard library; and functionality contained in snarkjs, such as the Groth16 prover.

Circuits overview

An overview of the overall SMASP protocol can be found in zkSecurity’s first report for Silent Protocol. Here, we focus on introducing recurring concepts that provide context for understanding our findings below.

Encrypted anonymized asset transfers

To understand the flow of assets through Silent’s shielded pool, we focus on one example first: the deposit circuit.

Deposit. A public Ethereum account deposits assets into a shielded pool account.

  • The deposited amount is added to a shielded balance, without revealing that balance.
  • The sender’s shielded account is anonymized by making indistinguishable dummy updates to 7 other accounts.

The privacy properties of this method are achieved by combining a zero-knowledge proof with ElGamal encryption of balances.

The ElGamal ciphertext representing a balance is defined as

(C1,C2)=(rG,bG+rX)

Here, (C1,C2) is the ciphertext, r is encryption randomness, G is a public elliptic curve base point, b is the balance, and X is the public key of the account owner.

The deposit circuit performs two operations on ciphertexts:

  • The sender account is updated by adding the deposited amount a to the balance, exploiting additive homomorphism:
(C1,C2)(C1,C2+aG)
  • All 8 balance ciphertexts (on the sender’s and 7 decoy accounts) are updated with new randomness r:
(C1i,C2i)(C1i+rG,C2i+rXi)

The re-randomization step is what makes the sender’s balance update indistinguishable from updates to the 7 decoy accounts, which leave their balance in place. Note that we can perform both the re-randomization and the balance update without knowing the accounts owner’s private keys, and we also preserve their ability to decrypt their balances. Therefore, decoys can be real, active accounts by other users, which is necessary for this scheme to provide actual anonymity for the sender. We have to ensure that the sender knows their own private key, by proving that we can rederive their public key xG=X.

The ciphertexts before and after applying the deposit update are public inputs to the circuit, while the new randomness is a private input. The deposit amount is public as well, because the contract needs to equate it to the amount received from the sender’s Ethereum account. The circuit asserts correct execution of the ciphertext updates given above.

A second part of the circuit computes a senderHash, defined as

senderHash=H(rG+xXaml).

Here, H() is the Poseidon hash function, r is randomness, x is the sender’s private key and Xaml is the public key shared by the compliance committee. The senderHash and rG are made available as public inputs. Note that the key xXaml is shared between the sender and compliance committee. Both the sender and the compliance committee can recompute the senderHash to check whether this transaction belongs to the sender.

SMASP circuits

Other circuits to interact with the shielded pool make use of the same concepts outlined above for the deposit circuit, with slight variations:

DepositAndWithdraw. The withdraw logic shares its circuit with deposits, but uses a negative amount. The only difference, which is handled in-circuit, is that for withdrawals we also check that the amount is smaller than the balance. In order to do this, the circuit takes the current balance as private input and verifies that it is encrypted correctly.

Transfer differs from deposits in that the sent amount is private, and that two accounts are anonymously updated: the sender and recipient. The circuit likewise encrypts transaction details in two versions: One shared between sender and recipient, and one shared with the compliance committee. Both versions derive a shared secret using ECDH and use it as the key in MiMC-based encryption.

Register is the circuit that creates new shielded accounts. It generates new ElGamal ciphertexts of zero balances for four different assets.

FeeRegistration lets a user subscribe for zero-fee withdrawals. It uses the same technique as deposits to encrypt the end of the subscription period.

WithdrawFeeReduction is the method unlocked for a user after calling FeeRegistration. It is similar to withdraw, except that it also verifies the validity of the encrypted subscription period against the current block number, which is a public input.

TransferToNonSilent, ClaimAndRegister and ClaimBatchPoints are not used by smart contracts in the audited version of the protocol. They use the same techniques as Transfer and Register to verify encrypted balances and compute encrypted transaction details.

All of these circuits – with the exception of Register, ClaimAndRegister and ClaimBatchPoints – hide the sender in a size-8 anonymity set, using the same re-encryption technique as deposits.

Generally speaking, we found the implementation of encrypted and anonymized asset transfers to be solid, with consistent usage of the same patterns and well-documented core templates, like BalanceVerify() and BalanceUpdate(). Only one major issue was found in this part of the code, which stems from a non-standard application of ElGamal encryption in the FeeRegistration logic, breaking the sender’s anonymity; see finding #00.

Secret sharing for compliance

After joining the compliance committee, every member will create a secret that they share with all other members. Likewise, the other members send a secret share to them. The scheme uses a variant of Shamir secret sharing that is suitable for threshold decryption, by avoiding the need for a single dealer; it also ensures that secret shares are verifiable against public commitments to the secret generated by each member. See AHS20 for an overview of the scheme.

The SecretSharing template represents the scheme in circuit form. The template is used by a committee member when they share secrets with other members, by posting them in encrypted form to a compliance smart contract. Along with encrypted shares, commitments to the underlying secret are also posted publicly; this enables members to verify their shares. The circuit’s purpose is to prove that encrypted shares and commitments are computed correctly and from the same polynomial.

In mathematical terms, the secret is an element of a finite field, s𝔽p. The sharing entity constructs a polynomial p(X) of degree T1 which evaluates to the secret at 0:

p(X)=j=0T1pjXj,where s=p0=p(0).

Polynomial coefficients are passed as private inputs, along with S public evaluation points xi, i=0,,S1. The SecretSharing template evaluates the polynomial at each xi to obtain the ith secret share, ssi=p(xi). Note that ST (and uniqueness of the xi) ensures that T or more shares can be combined to reconstruct the secret. In practice, members will only combine shares “in the exponent” so as to not reveal them, to collectively compute a curve point sC for ElGamal decryption.

Besides evaluating the polynomial, the template also needs to compute commitments to the polynomial coefficients, which are defined as

Aj=pjG.

The Aj are broadcast by storing them on the smart contract. To validate the share they received, each member can check that

ssiG=p(xi)G=j=0T1xijAj.

To do scalar multiplications pjG efficiently in the circuit, the chosen curve is BabyJubJub, whose base field is the native circuit field. Note, however, that the polynomial lives in the scalar field of that curve. This means we have to perform polynomial operations in non-native arithmetic modulo the curve order p; with coefficients, evaluation points and secret shares all represented as bigints.

Non-native arithmetic is a major source of complexity in the SecretSharing template. Indeed, out of the 6 high-to-medium findings reported, 5 are related to SecretSharing and non-native arithmetic (see findings #01 through #05).