Solidity

Overview

  • Pip.sol contract has 2 main functions: deposit() and withdraw() . Anyone can check a proof with checkProof() to see if the proof is valid and if it has been used to withdraw yet. Owner can withdraw fees accrued via withdrawFees() .

  • PoseidonT3.sol on-chain hasher library is used to poseidon hash stuff.

  • PlonkVerifierPPOT.sol is the on-chain PLONK verifier created using PPoT phase 1 file. See more here: PLONK & PPoT.

Design Choices & Notes

  • Combined PRC20 & PLS functionality into one contract seamlessly by denoting PLS interactions with address(0) token address.

  • ZERO_VALUE exists to fill up sibling nodes with default zero values for tree nodes that are untouched, to reduce computation.

  • Merkle tree data structure is used to store commitments because it's the most efficient, and the only plausible option when dealing with circuits. Using a mapping requires iteration through keys, which would be impossible in a circuit. The same for arrays, always exhaustively having to iterate through the array. With a merkle tree, we can always do the proof in a log(n) succinct manner to prove the commitment exists for a particular root.

  • System allows for multiple merkle trees to be generated. This reduces gas burden on depositors.

  • All the PIP pools only require the single verifier. The verification process and circuit logic is denomination and token agnostic because the circuit is only concerned with constraining witness inputs to create a valid merkle tree.

  • Proofs generated off-chain always use the latest root in the system to keep depositor-recipient anonymous. If the same deposit root was used during a withdraw, observers could easily speculate that the recipient is the person who deposited at that specific old root. By always using the latest root, this problem is solved.

  • Given the public values nullifierHash and commitment , it is impossible for any observer to compute the common nullifier which generated them. Mathematically, given Hash(X, 1) and Hash(X, 2), finding X cannot be done faster than brute force.

  • The nullifierHash is the core element that prevents double spends. The beauty is that regardless of the root, regardless of the recipient, the nullifierHash stays the SAME, and there is only ONE per unique deposit, since it is derived from the user's single nullifier and the specific leafIndex which is set in stone.

  • Given a nullifierHash , nobody can determine which leaf it corresponds to, unless they also know the nullifier.

  • Proofs cannot be tampered during a withdraw because the witness inputs used to generate that proof take ALL the input signals as seen in the circuit. Any tampering of the witness inputs leads to an invalid proof, assuming there are no unconstrained intermediary signals in the circuit, which there are none.

    • For example, nobody can change the recipient , gas , or fee values when calling withdraw() because the verifyProof() function uses bilinear pairing checks against all witness inputs to regenerate the proof.

  • The siblingNodes mapping can remain empty and doesn't need to be filled during construction. The first deposit properly sets the sibling nodes. Because the first deposit's path indices are all 0, at every level up the tree they will get set to a new value, per _insertCommitment() .

  • There was no reason to generate a genesis root value during construction, this happens naturally during the first deposit.

  • If someone deposits right when a withdraw is about to occur, everything functions normally. The withdraw is using "snapshotted" data at a point in time from the merkle tree, which will always be correct. At every point in the life of a merkle tree, it's data is permanent and true.

  • Generating a proof off chain is done in the user's browser. The finalized proof can be safely viewed publicly because it only displays the public signals and EC points. The EC points have taken into account all the public and private signals, but nobody can decipher the private signals from the EC points.

  • Frontend uses window.crypto.getRandomValues() which uses OS secure RNG based on thermal noise and hardware events to generate the nullifier.

Last updated