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
Here, is the ciphertext, is encryption randomness, is a public elliptic curve base point, is the balance, and 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 to the balance, exploiting additive homomorphism:
- All 8 balance ciphertexts (on the sender’s and 7 decoy accounts) are updated with new randomness :
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 .
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
Here, is the Poseidon hash function, is randomness, is the sender’s private key and is the public key shared by the compliance committee. The senderHash
and are made available as public inputs. Note that the key 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, . The sharing entity constructs a polynomial of degree which evaluates to the secret at 0:
Polynomial coefficients are passed as private inputs, along with public evaluation points , . The SecretSharing
template evaluates the polynomial at each to obtain the th secret share, . Note that (and uniqueness of the ) ensures that 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 for ElGamal decryption.
Besides evaluating the polynomial, the template also needs to compute commitments to the polynomial coefficients, which are defined as
The are broadcast by storing them on the smart contract. To validate the share they received, each member can check that
To do scalar multiplications 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 ; 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).