Aztec
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Help
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
2
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Aztec Connect Claim Proof Bug ## TLDR + On the 12th of September we received a submission to our bug bounty program on Immunefi claiming Aztec Connect had a critical bug in one of its core ZK (zero knowledge) circuits. + On the same day we assessed the validity of the submission and took preventative measures (disabling public access to the workflow that could exploit this issue) + On the 3rd of October we patched the target system + On the 10th of October we paid the bounty of $450,000 USD to the white hat The bug was discovered by security researcher [lucash-dev](https://github.com/lucash-dev) ## Details Aztec Connect is a privacy-focused zkRollup system optimized for DeFi use-cases. It batches private transactions within its system using zero knowledge proofs, enabling users to send funds between each and to Layer 1 smart contracts privately. One of its novel features was DeFi interactions allowing users to exchange tokens or invest in protocols. As an example of how Aztec Connect works, let's say several users want to invest in the same L1 DeFi protocol: 1. Each of the users submits a DeFi interaction deposit proof to the mempool. The proof keeps the identity of the user private, but the target protocol and investment amount are known. 2. The sequencer groups intended interactions with the same protocol together. Once there are enough intents to split L1 costs efficiently, the system computes and submits a rollup proof. 3. Having received the rollup proof, the smart contract acts on behalf of the users and performs the actual investment or exchange, receiving tokens in return. 4. On the next rollup the sequencer completes special claim proofs, splitting the newly received tokens between users. The bug uncovered was in the claim proof and could only be exploited by a sequencer (for example, through the escape hatch). The core vulnerability was the calculation of the final output of a given user's DeFi interaction(i.e. how many tokens they received after a DeFi interaction), which could be spoofed by a malicious sequencer. If this were a program written in rust or C, then a simple check that the $\mathit{result} = \mathit{total\_output}\frac{\mathit{user\_input}}{\mathit{total\_input}}$ in integers could be run. Unfortunately zero knowledge circuits don't operate over integers but finite fields. Because the fundamental units in zero knowledge circuits are finite fields, sophisticated tricks have to be implemented to run simple integer operations. If you are familiar with finite field arithmetic and ZK Circuits you can skip the next session and go right to the explanation of the bug. ## ZK Circuit Background A mini-tutorial on finite fields modulo prime numbers: + The finite field has a modulus $p$ which in our case is a prime number (can only be divided by itself and 1). We can use 5, we can't use 6 + All the elements in the finite field are integers from 0 to $p-1$. For $\mathbb{F}_5$ the elements are $\{0,1,2,3,4\}$ + We can perform addition. If the sum of two elements is more or equal to the modulus, we subtract the modulus from the result until it fits $3+4= 2\ mod\ 5$ + Negating an element is just finding an element that when summed with the original one produces 0 $1 + 4 = 0\ mod\ 5$ + Multiplication is performed in the same way, just multiply and then remove $p$ until the result fits $3\cdot 3 = 4\ mod\ 5$ + The inverse is an element that becomes 1 when multiplied by the original $4\cdot 4 = 1\ mod\ 5$, 0 doesn't have an inverse, every other element has an inverse. + Subtraction is just adding a negated element and division is multiplication by the inverse. ZK circuits consist of connected gates using finite field arithmetic. Aztec Connect used TurboPlonk--which you can think about as an extended instruction set for Plonk. Each gate has the form $q_m\cdot w_l\cdot w_r + q_1\cdot w_l + q_2\cdot w_r + q_3\cdot w_o + q_c = 0\ mod\ p$ The $q_m$,$q_1$, $q_2$,$q_3$, $q_c$ are selectors. They are chosen by the circuit writer and define the logic of the circuit. $w_l$,$w_r$, $w_o$ are witness values. You can think about them as the intermediate states of a program being executed. The circuit consists of many gates like these, where individual $w$ are connected to each other in various ways, like in the image. Some are also connected to inputs and outputs of the circuit. ![circuit](https://hackmd.io/_uploads/ryiwZDEGT.png) For example, if we wanted to show that $y=4x^3 +2$ we could produce the following gates: 1. $w_l\cdot w_r - w_o=0\ mod\ p$ 2. $4\cdot w_l\cdot w_r - w_o + 2 = 0 \ mod\ p$ We connect $w_l$ and $w_r$ at the first gate and $w_l$ at the second gate together so that they represent the same value $x$ and connect the $w_o$ at the first gate to $w_r$ at the second gate. Then $w_o$ at the second gate becomes $y$. This is easy to check: 1. $x\cdot x - x^2=0 \ mod\ p$ 2. $4\cdot x\cdot x^2 - y +2 = 0\ mod\ p$ So this set of gates with the proper wiring computes exactly what we wanted. There are some standard gates that are used quite often : 1. $w_l\cdot w_r - w_r=0\ mod\ p$, where $w_l=w_r$ through wiring. Boolean gate $x\cdot (x-1)=0\ mod\ p$ ensuring $x$ is zero or 1. 2. $w_l+w_r-w_o = 0\ mod\ p$ Standard addition gate. 3. $w_l\cdot w_r - w_o=0\ mod\ p$ Standard multiplication gate. 4. $w_l\cdot w_r - 1=0 \ mod\ p$ Ensures that $w_l$ is non-zero. 5. $w_{l1}\cdot w_{r1} - 1 =0$ along with $w_{l2}\cdot w_{r2} - w_{o2} = 0$ where $w_r$ are connected to each over Represents that $w_{o2}=\frac{w_{l2}}{w_{l1}}$ The last gate is actually a nice example of the core concept of circuits: you don't have to prove the computation, you just need to provide the witness for the correctness. This is the case with division in finite fields. Finding the inverse is a heavy operation, but showing that you've found the inverse is trivial: just show that the product of the two values is 1. ## Integer arithmetic over fields So, going back to our problem: we needed to show that $\mathit{user\_output}=\mathit{total\_output}\cdot\frac{\mathit{user\_input}}{\mathit{total\_input}}$ in integers, but we have to do this in a circuit. Well, one of the problems is that the $user\_output$ will be a floored value in most cases, since we are using integer division. $user\_output\cdot total\_input \le total\_output \cdot user\_input$ So we need to introduce a division remainder term: $$user\_output\cdot total\_input + remainder= total\_output\cdot user\_input$$ This enables us to satisfy the constraint, but, unfortunately, now we can change the value of $user\_output$ to any value while $total\_input$, $total\_output$ and $user\_input$ are set by varying the remainder. We need to ensure that this relation is correct in integers, not in field elements. For that purpose we split each of the values into 68-bit limbs and check the correctness of the whole relation the same way one would check the correctness of an equation in elementary school: $12 \cdot 12 = 11 \cdot 13 + 1$ First we check the lower digits: $2\cdot 2 = 1\cdot 3 + 1$ If there is a carry, we add it to the tens: $2 \cdot 1 + 1\cdot 2 = 1\cdot 3 +1\cdot 1$ And check the hundreds: $1\cdot 1= 1\cdot 1$ When we do this computation, we only have to compute the products of individual digits in memory so that we don't overflow it. The way we implemented integer checks for the claim ratio equation is completely the same, but the limbs (digits) are in $[0, 2^{68}-1]$ instead of $[0,9]$. And each number consists of 4 such limbs. Here we have the piece of code performing this check: ```c++ // takes a [204-208]-bit limb and splits it into a low 136-bit limb and a high 72-bit limb const auto split_out_carry_term = [&composer, &shift_2](const field_ct& limb) { const uint256_t limb_val = limb.get_value(); const uint256_t lo_val = limb_val.slice(0, 68 * 2); const uint256_t hi_val = limb_val.slice(68 * 2, 256); const field_ct lo(witness_ct(&composer, lo_val)); const field_ct hi(witness_ct(&composer, hi_val)); lo.create_range_constraint(68 * 2); hi.create_range_constraint(72); // allow for 4 overflow bits limb.assert_equal(lo + (hi * shift_2)); return std::array<field_ct, 2>{ lo, hi }; }; // Use schoolbook multiplication algorithm to multiply 2 4-limbed values together, then convert result into 4 // 2-limb values (with limbs twice the size) that do not overlap const auto compute_product_limbs = [&split_out_carry_term, &shift_1](const std::array<field_ct, 4>& left, const std::array<field_ct, 4>& right, const std::array<field_ct, 4>& to_add, const bool use_residual = false) { // a = left[0] * right[0]; const field_ct b = left[0].madd(right[1], left[1] * right[0]); const field_ct c = left[0].madd(right[2], left[1].madd(right[1], left[2] * right[0])); const field_ct d = left[0].madd(right[3], left[1].madd(right[2], left[2].madd(right[1], left[3] * right[0]))); const field_ct e = left[1].madd(right[3], left[2].madd(right[2], left[3] * right[1])); const field_ct f = left[2].madd(right[3], left[3] * right[2]); // g = left[3] * right[3]; if (use_residual) { const auto t0 = split_out_carry_term(to_add[0] + left[0].madd(right[0], (b * shift_1) + to_add[1] * shift_1)); const auto r0 = t0[0]; const auto t1 = split_out_carry_term(t0[1].add_two(c + to_add[2], to_add[3] * shift_1 + d * shift_1)); const auto r1 = t1[0]; const auto t2 = split_out_carry_term(t1[1].add_two(e, f * shift_1)); const auto r2 = t2[0]; const auto r3 = left[3].madd(right[3], t2[1]); return std::array<field_ct, 4>{ r0, r1, r2, r3 }; } const auto t0 = split_out_carry_term(left[0].madd(right[0], (b * shift_1))); const auto r0 = t0[0]; const auto t1 = split_out_carry_term(t0[1].add_two(c, d * shift_1)); const auto r1 = t1[0]; const auto t2 = split_out_carry_term(t1[1].add_two(e, f * shift_1)); const auto r2 = t2[0]; const auto r3 = left[3].madd(right[3], t2[1]); return std::array<field_ct, 4>{ r0, r1, r2, r3 }; }; const auto lhs = compute_product_limbs(left_1, right_1, { 0, 0, 0, 0 }, false); const auto rhs = compute_product_limbs(left_2, right_2, residual_limbs, true); bool_ct balanced(&composer, true); for (size_t i = 0; i < 4; ++i) { balanced = balanced && lhs[i] == rhs[i]; } return balanced; ``` So all we did was split each value into 4 limbs like this: $value = value_0 + value_1\cdot 2^{68} + value_2\cdot 2^{136} + value_3\cdot 2^{204}\ mod\ p$ Each of the limbs was then range constrained to 68-bits to ensure correct decomposition (range constraints can be easily implemented by combining the witnesses from boolean gates). The vulnerability stemmed from two core issues: 1. The top limb was constrained to 68 bits 2. The remainder had no range constraints at all ```c++ // Split a field_t element into 4 68-bit limbs const auto split_into_limbs = [&composer, &shift_1, &shift_2, &shift_3](const field_ct& input) { const uint256_t value = input.get_value(); const uint256_t t0 = value.slice(0, 68); const uint256_t t1 = value.slice(68, 136); const uint256_t t2 = value.slice(136, 204); const uint256_t t3 = value.slice(204, 272); std::array<field_ct, 4> limbs{ witness_ct(&composer, t0), witness_ct(&composer, t1), witness_ct(&composer, t2), witness_ct(&composer, t3), }; field_ct limb_sum_1 = limbs[0].add_two(limbs[1] * shift_1, limbs[2] * shift_2); field_ct limb_sum_2 = input - (limbs[3] * shift_3); limb_sum_1.assert_equal(limb_sum_2); limbs[0].create_range_constraint(68); limbs[1].create_range_constraint(68); limbs[2].create_range_constraint(68); limbs[3].create_range_constraint(68); // The offending range constraint return limbs; }; ``` If you are range constraining a value that needs to be in the range $2^{252}$ to $2^{272}$ via decomposition, you lose the 1-to-1 correspondence that would've happened if the range constraint was $2^{252}$. There are now $2^{20}$ possible values of limbs of each of the original values. More concretely, since all the arithmetic is happening modulo $p$, you can add this modulus to the decomposition and when the value is reconstructed that extra $p$ is going to disappear, so instead of the original claim ratio equation in integers we can update it to: $$(user\_output +a\cdot p)\cdot (total\_input +b\cdot p) + remainder + c \cdot p = (total\_output + d \cdot p) \cdot (user\_input + e\cdot p)$$ For simplicity, let's say $user\_input=1$, then we can set $a=0$, $b=0$, $c=0$, $d=1$, $e=0$, and the resulting equation is: $user\_output\cdot total\_input + remainder = total\_output + p$ Then, we can set: $remainder = total\_output + p - user\_output \cdot total\_input$ Now we can vary $user\_output$ and as long as $user\_output\cdot total\_input > total\_output$ and $user\_output \cdot total\_input - total\_output \le p$ we'll be able to compute an appropriate remainder, since it is not constrained and controlling user input gives us a mechanism to steal TVL. # The Patch # The first and most important issue is to stop multiples of $p$ being added to various parts of the ratio equation. For that we simply started range constraining individual limbs so that there is a 1-to-1 map between original and split values. ```c++ // Split a field_t element into 4 68-bit limbs const auto split_into_limbs = [&composer, &shift_1, &shift_2, &shift_3](const field_ct& input, const size_t MAX_INPUT_BITS) { const uint256_t value = input.get_value(); constexpr size_t NUM_BITS_PER_LIMB = 68; ASSERT(MAX_INPUT_BITS <= MAX_NO_WRAP_INTEGER_BIT_LENGTH); ASSERT(MAX_INPUT_BITS > 0); const uint256_t t0 = value.slice(0, NUM_BITS_PER_LIMB); const uint256_t t1 = value.slice(NUM_BITS_PER_LIMB, 2 * NUM_BITS_PER_LIMB); const uint256_t t2 = value.slice(2 * NUM_BITS_PER_LIMB, 3 * NUM_BITS_PER_LIMB); const uint256_t t3 = value.slice(3 * NUM_BITS_PER_LIMB, 4 * NUM_BITS_PER_LIMB); std::array<field_ct, 4> limbs{ witness_ct(&composer, t0), witness_ct(&composer, t1), witness_ct(&composer, t2), witness_ct(&composer, t3), }; field_ct limb_sum_1 = limbs[0].add_two(limbs[1] * shift_1, limbs[2] * shift_2); field_ct limb_sum_2 = input - (limbs[3] * shift_3); limb_sum_1.assert_equal(limb_sum_2); // Since the modulus is a power of two minus one, we can simply range constrain each of the limbs size_t bits_left = MAX_INPUT_BITS; for (size_t i = 0; i < 4; i++) { // If we've run out of bits, enforce zero if (bits_left == 0) { limbs[i].assert_is_zero(); // If there are not enough bits for a full lmb, reduce constraint } else if (bits_left < NUM_BITS_PER_LIMB) { limbs[i].create_range_constraint(bits_left); bits_left = 0; } else { // Just a regular limb limbs[i].create_range_constraint(NUM_BITS_PER_LIMB); bits_left -= NUM_BITS_PER_LIMB; } } return limbs; }; ``` Another issue that we noticed was that if we left the remainder unconstrained, but still constrained the limbs, the sequencer could possibly create a proof that would assign the depositor much less funds than they were supposed to receive. If we look at the equation, it will become obvious that we can increase the remainder in steps equal to $total\_input$ to decrease $user\_output$. $$user\_output\cdot total\_input + remainder= total\_output\cdot user\_input$$ So we also added a range constraint ensuring $remainder \in [0,total\_input)$ ```c++ residual.create_range_constraint(notes::NOTE_VALUE_BIT_LENGTH, "ratio_check range constraint failure: residual"); // We need to assert that residual < a2 // i.e. a2 - residual > 0 => a2 - residual - 1 >= 0 (ratios.a2 - residual - 1) .normalize() .create_range_constraint(notes::NOTE_VALUE_BIT_LENGTH, "ratio_check range constraint failure: residual >= a2"); ``` ## Conclusion Any project as large as Aztec Connect requires a great deal of attention to implement securely and while the code was extensively audited this bug got through. So what can be done to minimize the probability of such bugs in future systems? On our part, Aztec Labs is implementing a few process improvements to prevent future bugs of this type: 1. Implementing a policy where all ad-hoc implementations of primitives are prohibited. If there is a need to do integer comparisons, then the code must go into the standard library, where it is heavily scrutinised. 2. Creating automated tooling to detect unconstrained values and help focus auditors' attention specifically on them and derived values. Minimizing the impact of human error is absolutely crucial to building a robust system, so there is huge value in redundancy and automation. We credit independent security researcher [lucash-dev](https://github.com/lucash-dev) with discovering the vulnerability. His vigilance--as well as the hard work of the greater zero knowledge community--ensure the safety of open-source crypto systems.

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully