mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-07 00:33:51 -05:00
feat: entrypoint upgrade (#82)
Signed-off-by: Ameen Soleimani <ameen.sol@gmail.com> Co-authored-by: Ameen Soleimani <ameen.sol@gmail.com>
This commit is contained in:
289
audit/entrypoint_upgrade_audit_oxorio.md
Normal file
289
audit/entrypoint_upgrade_audit_oxorio.md
Normal file
@@ -0,0 +1,289 @@
|
||||
---
|
||||
Logo:
|
||||
Date: May 20, 2025
|
||||
---
|
||||
|
||||
# Privacy Pools smart contracts audit report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### Executive Summary
|
||||
This document presents the smart contracts security audit conducted by Oxorio for Privacy Pool Smart Contracts.
|
||||
Privacy Pool is a blockchain protocol that enables private asset transfers. Users can deposit funds publicly and partially withdraw them privately, provided they can prove membership in an approved set of addresses.
|
||||
|
||||
The audit process involved a comprehensive approach, including manual code review, automated analysis, and extensive testing and simulations of the curcuits to assess the project’s security and functionality. The audit covered a total of 2 contracts, encompassing 794 lines of code. For an in-depth explanation of used the smart contract security audit methodology, please refer to the [Security Assessment Methodology](#security-assessment-methodology) section of this document.
|
||||
|
||||
### Summary of findings
|
||||
The table below provides a comprehensive summary of the audit findings, categorizing each by status and severity level. For a detailed description of the severity levels and statuses of findings, see the [Findings Classification Reference](#findings-classification-reference) section.
|
||||
|
||||
Detailed technical information on the audit findings, along with our recommendations for addressing them, is provided in the [Findings Report](#findings-report) section for further reference.
|
||||
|
||||
|
||||
[FINDINGS]
|
||||
|
||||
## Audit Overview
|
||||
|
||||
[TOC]
|
||||
|
||||
### Disclaimer
|
||||
At the request of the client, Oxorio consents to the public release of this audit report. The information contained herein is provided "as is" without any representations or warranties of any kind. Oxorio disclaims all liability for any damages arising from or related to the use of this audit report. Oxorio retains copyright over the contents of this report.
|
||||
|
||||
This report is based on the scope of materials and documentation provided to Oxorio for the security audit as detailed in the Executive Summary and Audited Files sections. The findings presented in this report may not encompass all potential vulnerabilities. Oxorio delivers this report and its findings on an as-is basis, and any reliance on this report is undertaken at the user’s sole risk. It is important to recognize that blockchain technology remains in a developmental stage and is subject to inherent risks and flaws.
|
||||
|
||||
This audit does not extend beyond the programming language of smart contracts to include areas such as the compiler layer or other components that may introduce security risks. Consequently, this report should not be interpreted as an endorsement of any project or team, nor does it guarantee the security of the project under review.
|
||||
|
||||
THE CONTENT OF THIS REPORT, INCLUDING ITS ACCESS AND/OR USE, AS WELL AS ANY ASSOCIATED SERVICES OR MATERIALS, MUST NOT BE CONSIDERED OR RELIED UPON AS FINANCIAL, INVESTMENT, TAX, LEGAL, REGULATORY, OR OTHER PROFESSIONAL ADVICE. Third parties should not rely on this report for making any decisions, including the purchase or sale of any product, service, or asset. Oxorio expressly disclaims any liability related to the report, its contents, and any associated services, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Oxorio does not warrant, endorse, or take responsibility for any product or service referenced or linked within this report.
|
||||
|
||||
For any decisions related to financial, legal, regulatory, or other professional advice, users are strongly encouraged to consult with qualified professionals.
|
||||
|
||||
### Project Brief
|
||||
|
||||
| Title | Description |
|
||||
| --- | --- |
|
||||
| Client | Privacy Pools |
|
||||
| Project name | Privacy Pools smart contracts |
|
||||
| Category | privacy, asset management |
|
||||
| Repository | [github.com/0xbow-io/privacy-pools-core](https://github.com/0xbow-io/privacy-pools-core) |
|
||||
| Documentation | [README.md](https://github.com/0xbow-io/privacy-pools-core/tree/238e3594053becde68aa40e1e4cef6c0c46e68da/docs) |
|
||||
| Initial commit | [`238e3594053becde68aa40e1e4cef6c0c46e68da`](https://github.com/0xbow-io/privacy-pools-core/commit/238e3594053becde68aa40e1e4cef6c0c46e68da) |
|
||||
| Final commit | [`238e3594053becde68aa40e1e4cef6c0c46e68da`](https://github.com/0xbow-io/privacy-pools-core/commit/238e3594053becde68aa40e1e4cef6c0c46e68da) |
|
||||
| Languages | Solidity |
|
||||
| Lead Auditor | Alexander Mazaletskiy - [am@oxor.io](emailto:am@oxor.io) |
|
||||
| Project Manager | Elena Kozmiryuk - [elena@oxor.io](mailto:elena@oxor.io) |
|
||||
|
||||
### Project Timeline
|
||||
The key events and milestones of the project are outlined below.
|
||||
|
||||
| Date | Event |
|
||||
| --- | --- |
|
||||
| May 15, 2025 | Client approached Oxorio requesting an audit. |
|
||||
| May 18, 2025 | The audit team commenced work on the project. |
|
||||
| May 18, 2025 | Submission of the comprehensive report. |
|
||||
|
||||
|
||||
### Audited Files
|
||||
|
||||
The following table contains a list of the audited files. The [scc](https://github.com/boyter/scc) tool was used to count the number of lines and assess complexity of the files.
|
||||
|
||||
| | File | Lines | Blanks | Comments | Code | Complexity |
|
||||
| - | - | - | - | - | - | - |
|
||||
| 1 | [packages/contracts/src/contracts/Entrypoint.sol](https://github.com/0xbow-io/privacy-pools-core//blob/238e3594053becde68aa40e1e4cef6c0c46e68da/packages/contracts/src/contracts/Entrypoint.sol) | 403 | 78 | 142 | 183 | 33% |
|
||||
| 2 | [packages/contracts/src/interfaces/IEntrypoint.sol](https://github.com/0xbow-io/privacy-pools-core//blob/238e3594053becde68aa40e1e4cef6c0c46e68da/packages/contracts/src/interfaces/IEntrypoint.sol) | 391 | 54 | 242 | 95 | 0% |
|
||||
| | **Total** | 794 | 132 | 384 | 278 | 22% |
|
||||
|
||||
**Lines:** The total number of lines in each file. This provides a quick overview of the file size and its contents.
|
||||
|
||||
**Blanks:** The count of blank lines in the file.
|
||||
|
||||
**Comments:** This column shows the number of lines that are comments.
|
||||
|
||||
**Code:** The count of lines that actually contain executable code. This metric is essential for understanding how much of the file is dedicated to operational elements rather than comments or whitespace.
|
||||
|
||||
**Complexity**: This column shows the file complexity per line of code. It is calculated by dividing the file's total complexity (an approximation of [cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) that estimates logical depth and decision points like loops and conditional branches) by the number of executable lines of code. A higher value suggests greater complexity per line, indicating areas with concentrated logic.
|
||||
|
||||
### Project Overview
|
||||
The protocol enables users to deposit assets publicly and withdraw them privately, provided they can prove membership in an approved set of addresses. Each supported asset (native or ERC20) has its own dedicated pool contract that inherits from a common `PrivacyPool` implementation.
|
||||
|
||||
Deposit Flow
|
||||
|
||||
When a user deposits funds, they:
|
||||
|
||||
1. Generate commitment parameters (nullifier and secret)
|
||||
2. Send the deposit transaction through the Entrypoint
|
||||
3. The Entrypoint routes the deposit to the appropriate pool
|
||||
4. The pool records the commitment in its state tree
|
||||
5. The depositor receives a deposit identifier (label) and a commitment hash
|
||||
|
||||
Withdrawal Flow
|
||||
|
||||
To withdraw funds privately, users:
|
||||
|
||||
1. Generate a zero-knowledge proof demonstrating:
|
||||
- Ownership of a valid deposit commitment
|
||||
- Membership in the approved address set
|
||||
- Correctness of the withdrawal amount
|
||||
2. Submit the withdrawal transaction through a relayer
|
||||
3. The pool verifies the proof and processes the withdrawal
|
||||
4. A new commitment is created for the remaining funds (even if it is zero)
|
||||
|
||||
Pull Request Description
|
||||
|
||||
This pull request introduces changes to the to-be-upgraded `Entrypoint` contract. With this upgrade, a `usedPrecommitments` mapping is added to the contract, which tracks all used precommitments and prevents users from depositing using an already used precommitment, thus preventing stuck funds.
|
||||
|
||||
Introduced changes:
|
||||
|
||||
* state variable and checks added to Entrypoint
|
||||
* added unit tests for the new revert case
|
||||
* added an upgrade test using a forked environment from Ethereum Mainnet
|
||||
|
||||
An issue of this kind could theoretically happen to the secrets used on withdrawals, though it’s way less probable because of the nature of the commitment system on withdrawals:
|
||||
|
||||
1. When withdrawing, partially or completely, a previous existing commitment is spent and a new one is created for the remaining value (even if it’s zero). Because of this, when generating the withdrawal proof, the user must provide the nullifier of the spending commitment and a new nullifier for the new commitment. The `withdraw.circom` [circuit does take this in count](https://github.com/0xbow-io/privacy-pools-core/blob/6b38184fec2ad8d85a5adf0796d30d06426db124/packages/circuits/circuits/withdraw.circom#L94) and will never produce a withdrawal proof with a new commitment with the same nullifier as the just spent one.
|
||||
2. The secrets for deposits and withdrawals are generated differently. Both are created deterministically based on a `master_nullifier` and `master_secret`, but the data used as pre-image of the ultimate secret values is inherently different. For [deposits](https://github.com/0xbow-io/privacy-pools-core/blob/6b38184fec2ad8d85a5adf0796d30d06426db124/packages/sdk/src/core/account.service.ts#L241), the secrets are image of `poseidon(master_key, pool_scope, deposit_index)`, while the ones of [withdrawals](https://github.com/0xbow-io/privacy-pools-core/blob/6b38184fec2ad8d85a5adf0796d30d06426db124/packages/sdk/src/core/account.service.ts#L276) are the image of `poseidon(master_key, deposit_label, withdrawal_index)`. This makes the chances of collision of deposit and withdrawal secrets almost non-existent.
|
||||
3. When withdrawing, since a commitment is spent, its nullifier is marked as spent and that is checked for all further commitments to avoid double-spending. If a user were to submit two partial withdrawals of the same origin deposit quickly with an outdated state, one withdrawal transaction would be successful and the second one would revert, as both withdrawals would be trying to spend the same commitment, which can only be spent once.
|
||||
|
||||
This issue can not be used by a third actor in a malicious way whatsoever. Another account can see the chain and use your same pre-commitment for a deposit, but only the user who owns the master keys generated by the seed-phrase is the one that will be able to later spend the commitment.
|
||||
|
||||
### Findings Breakdown by File
|
||||
This table provides an overview of the findings across the audited files, categorized by severity level. It serves as a useful tool for identifying areas that may require attention, helping to prioritize remediation efforts, and provides a clear summary of the audit results.
|
||||
|
||||
[FINDINGS_SCOPE]
|
||||
|
||||
### Conclusion
|
||||
A comprehensive audit was conducted on 2 contracts, revealing no critical and major issues. However, several warnings and informational notes were identified. The audit identified vulnerability with already used nullifier and code optimization.
|
||||
|
||||
Following our initial audit, Privacy Pools worked closely with our team to address the identified issues. The proposed changes aim to strengthen protocol security, improve efficiency, and ensure seamless user experience. Key recommendations include adding validation checks, optimizing code, and ensuring compatibility between deposited and withdrawn values to enhance security and maintain user privacy.
|
||||
As a result, the project has passed our audit. Our auditors have verified that the Privacy Pools Smart Contracts, as of audited commit [`238e3594053becde68aa40e1e4cef6c0c46e68da`](https://github.com/0xbow-io/privacy-pools-core/commit/238e3594053becde68aa40e1e4cef6c0c46e68da), operates as intended within the defined scope, based on the information and code provided at the time of evaluation. The robustness of the codebase has been significantly improved, meeting the necessary security and functionality requirements established for this audit.
|
||||
|
||||
## Findings Report
|
||||
|
||||
### CRITICAL
|
||||
|
||||
No critical issues found.
|
||||
|
||||
### MAJOR
|
||||
|
||||
No major issues found.
|
||||
|
||||
### WARNING
|
||||
|
||||
#### [ACKNOWLEDGED] Potential nullifier reuse vulnerability in precommitment tracking in `EntryPoint`
|
||||
##### Location
|
||||
File | Location | Line
|
||||
--- | --- | ---
|
||||
[Entrypoint.sol](https://github.com/0xbow-io/privacy-pools-core/blob/238e3594053becde68aa40e1e4cef6c0c46e68da/packages/contracts/src/contracts/Entrypoint.sol#L317 "packages/contracts/src/contracts/Entrypoint.sol") | function `_handleDeposit` | 317
|
||||
|
||||
##### Description
|
||||
In the `_handleDeposit` function of the `EntryPoint` contract, a precommitment tracking mechanism has been implemented:
|
||||
|
||||
```solidity
|
||||
// Check if the `_precommitment` has already been used
|
||||
if (usedPrecommitments[_precommitment]) revert PrecommitmentAlreadyUsed();
|
||||
// Mark it as used
|
||||
usedPrecommitments[_precommitment] = true;
|
||||
```
|
||||
|
||||
This implementation only partially mitigates precommitment reuse risk. The vulnerability arises because a precommitment is computed as `hash(nullifier, secret)`. During withdrawal, the system marks the nullifier as spent. However, an attacker could use the same nullifier with different secrets to generate distinct precommitment hashes, effectively bypassing the precommitment reuse check.
|
||||
|
||||
This vulnerability creates a scenario where multiple deposits could be made using the same underlying nullifier, but only one could be successfully withdrawn (as the nullifier would be marked spent after the first withdrawal). The remaining deposits would become unwithdrawable, resulting in permanently locked funds.
|
||||
##### Recommendation
|
||||
We recommend reconsidering the deposit logic to address the fundamental nullifier reuse vulnerability. One possible solution is to implement a cryptographic mechanism to verify nullifier uniqueness without revealing it at deposit time.
|
||||
##### Update
|
||||
###### Client's response
|
||||
To use the same nullifier as other commitment, the attacker must have access to the user's Privacy Pool seedphrase, which gives access to spending all user's commitments. On top of it, nullifiers and secrets are generated based on a random key-pair and more pseudo-random values like pool scope and deposit label, making it impossible for a nullifier to be generated twice accidentally. We won't take any action regarding this issue.
|
||||
|
||||
### INFO
|
||||
|
||||
#### [ACKNOWLEDGED] Non-optimal gas usage due to check order in `Entrypoint`
|
||||
##### Location
|
||||
File | Location | Line
|
||||
--- | --- | ---
|
||||
[Entrypoint.sol](https://github.com/0xbow-io/privacy-pools-core/blob/238e3594053becde68aa40e1e4cef6c0c46e68da/packages/contracts/src/contracts/Entrypoint.sol#L113 "packages/contracts/src/contracts/Entrypoint.sol") | contract `Entrypoint` > function `deposit` | 113
|
||||
[Entrypoint.sol](https://github.com/0xbow-io/privacy-pools-core/blob/238e3594053becde68aa40e1e4cef6c0c46e68da/packages/contracts/src/contracts/Entrypoint.sol#L125 "packages/contracts/src/contracts/Entrypoint.sol") | contract `Entrypoint` > function `deposit` | 125
|
||||
|
||||
##### Description
|
||||
In the function `_handleDeposit` of contract `Entrypoint`, a check for the presence of `_precommitment` in the `usedPrecommitments` mapping was added. This function is invoked when calling the `deposit` functions for both native and non-native assets.
|
||||
|
||||
However, the `_precommitment` check is performed after storage is read to verify the `_pool` address:
|
||||
```solidity
|
||||
function _handleDeposit(IERC20 _asset, uint256 _value, uint256 _precommitment) internal returns (uint256 _commitment) {
|
||||
AssetConfig memory _config = assetConfig[_asset];
|
||||
IPrivacyPool _pool = _config.pool;
|
||||
if (address(_pool) == address(0)) revert PoolNotFound();
|
||||
|
||||
if (usedPrecommitments[_precommitment]) revert PrecommitmentAlreadyUsed();
|
||||
// ...
|
||||
```
|
||||
|
||||
and the asset transfer is executed in the `deposit` function:
|
||||
```solidity
|
||||
function deposit(
|
||||
IERC20 _asset,
|
||||
uint256 _value,
|
||||
uint256 _precommitment
|
||||
) external nonReentrant returns (uint256 _commitment) {
|
||||
_asset.safeTransferFrom(msg.sender, address(this), _value);
|
||||
_commitment = _handleDeposit(_asset, _value, _precommitment);
|
||||
}
|
||||
```
|
||||
|
||||
This leads to unnecessary gas consumption if the `_precommitment` already exists.
|
||||
##### Recommendation
|
||||
We recommend considering moving the `_precommitment` check to the very beginning of the `deposit` function logic to optimize gas usage.
|
||||
##### Update
|
||||
###### Client's response
|
||||
Will keep it as it is.
|
||||
## Appendix
|
||||
|
||||
### Security Assessment Methodology
|
||||
|
||||
Oxorio's smart contract security audit methodology is designed to ensure the security, reliability, and compliance of curcuits throughout their development lifecycle. Our process integrates the Smart Contract Security Verification Standard (SCSVS) with our advanced techniques to address complex security challenges. For a detailed look at our approach, please refer to the [full version of our methodology](https://docsend.com/view/yjpj6jggbqjpc5sa). Here is a concise overview of our auditing process:
|
||||
|
||||
**1. Project Architecture Review**
|
||||
|
||||
All necessary information about the smart contract is gathered, including its intended functionality and dependencies. This stage sets the foundation by reviewing documentation, business logic, and initial code analysis.
|
||||
|
||||
**2. Vulnerability Assessment**
|
||||
|
||||
This phase involves a deep dive into the smart contract's code to identify security vulnerabilities. Rigorous testing and review processes are applied to ensure robustness against potential attacks.
|
||||
|
||||
This stage is focused on identifying specific vulnerabilities within the smart contract code. It involves scanning and testing the code for known security weaknesses and patterns that could potentially be exploited by malicious actors.
|
||||
|
||||
**3. Security Model Evaluation**
|
||||
|
||||
The smart contract’s architecture is assessed to ensure it aligns with security best practices and does not introduce potential vulnerabilities. This includes reviewing how the contract integrates with external systems, its compliance with security best practices, and whether the overall design supports a secure operational environment.
|
||||
|
||||
This phase involves a analysis of the project's documentation, the consistency of business logic as documented versus implemented in the code, and any assumptions made during the design and development phases. It assesses if the contract's architectural design adequately addresses potential threats and integrates necessary security controls.
|
||||
|
||||
**4. Cross-Verification by Multiple Auditors**
|
||||
|
||||
Typically, the project is assessed by multiple auditors to ensure a diverse range of insights and thorough coverage. Findings from individual auditors are cross-checked to verify accuracy and completeness.
|
||||
|
||||
**5. Report Consolidation**
|
||||
|
||||
Findings from all auditors are consolidated into a single, comprehensive express audit. This report outlines potential vulnerabilities, areas for improvement, and an overall assessment of the smart contract’s security posture.
|
||||
|
||||
**6. Reaudit of Revised Submissions**
|
||||
|
||||
Post-review modifications made by the client are reassessed to ensure that all previously identified issues have been adequately addressed. This stage helps validate the effectiveness of the fixes applied.
|
||||
|
||||
**7. Final express audit Publication**
|
||||
|
||||
The final version of the express audit is delivered to the client and published on Oxorio's official website. This report includes detailed findings, recommendations for improvement, and an executive summary of the smart contract’s security status.
|
||||
|
||||
### Findings Classification Reference
|
||||
|
||||
#### Severity Level Reference
|
||||
The following severity levels were assigned to the issues described in the report:
|
||||
|
||||
| Title | Description |
|
||||
| --- | --- |
|
||||
| <span severity="CRITICAL">CRITICAL</span> | Issues that pose immediate and significant risks, potentially leading to asset theft, inaccessible funds, unauthorized transactions, or other substantial financial losses. These vulnerabilities represent serious flaws that could be exploited to compromise or control the entire contract. They require immediate attention and remediation to secure the system and prevent further exploitation. |
|
||||
| <span severity="MAJOR">MAJOR</span> | Issues that could cause a significant failure in the contract's functionality, potentially necessitating manual intervention to modify or replace the contract. These vulnerabilities may result in data corruption, malfunctioning logic, or prolonged downtime, requiring substantial operational changes to restore normal performance. While these issues do not immediately lead to financial losses, they compromise the reliability and security of the contract, demanding prioritized attention and remediation. |
|
||||
| <span severity="WARNING">WARNING</span> | Issues that might disrupt the contract's intended logic, affecting its correct functioning or making it vulnerable to Denial of Service (DDoS) attacks. These problems may result in the unintended triggering of conditions, edge cases, or interactions that could degrade the user experience or impede specific operations. While they do not pose immediate critical risks, they could impact contract reliability and require attention to prevent future vulnerabilities or disruptions. |
|
||||
| <span severity="INFO">INFO</span> | Issues that do not impact the security of the project but are reported to the client's team for improvement. They include recommendations related to code quality, gas optimization, and other minor adjustments that could enhance the project's overall performance and maintainability. |
|
||||
|
||||
#### Status Level Reference
|
||||
Based on the feedback received from the client's team regarding the list of findings discovered by the contractor, the following statuses were assigned to the findings:
|
||||
|
||||
| Title | Description |
|
||||
| --- | --- |
|
||||
| <span status="NEW">NEW</span> | Waiting for the project team's feedback. |
|
||||
| <span status="FIXED">FIXED</span> | Recommended fixes have been applied to the project code and the identified issue no longer affects the project's security. |
|
||||
| <span status="ACKNOWLEDGED">ACKNOWLEDGED</span> | The project team is aware of this finding and acknowledges the associated risks. This finding may affect the overall security of the project; however, based on the risk assessment, the team will decide whether to address it or leave it unchanged. |
|
||||
| <span status="NO ISSUE">NO ISSUE</span> | Finding does not affect the overall security of the project and does not violate the logic of its work. |
|
||||
|
||||
|
||||
### About Oxorio
|
||||
|
||||
OXORIO is a blockchain security firm that specializes in curcuits, zk-SNARK solutions, and security consulting. With a decade of blockchain development and five years in smart contract auditing, our expert team delivers premier security services for projects at any stage of maturity and development.
|
||||
|
||||
Since 2021, we've conducted key security audits for notable DeFi projects like Lido, 1Inch, Rarible, and deBridge, prioritizing excellence and long-term client relationships. Our co-founders, recognized by the Ethereum and Web3 Foundations, lead our continuous research to address new threats in the blockchain industry. Committed to the industry's trust and advancement, we contribute significantly to security standards and practices through our research and education work.
|
||||
|
||||
Our contacts:
|
||||
|
||||
- [oxor.io](https://oxor.io)
|
||||
- [ping@oxor.io](mailto:ping@oxor.io)
|
||||
- [Github](https://github.com/oxor-io)
|
||||
- [Linkedin](https://linkedin.com/company/0xorio)
|
||||
- [Twitter](https://twitter.com/0xorio)
|
||||
@@ -10,7 +10,7 @@ The protocol enables users to deposit assets publicly and withdraw them privatel
|
||||
|
||||
Entrypoint (Proxy): `0x6818809EefCe719E480a7526D76bD3e561526b46`
|
||||
|
||||
Entrypoint (Implementation): `0xdD8aA0560a08E39C0b3A84BBa356Bc025AfbD4C1`
|
||||
Entrypoint (Implementation): `0x6818809EefCe719E480a7526D76bD3e561526b46`
|
||||
|
||||
ETH Pool: `0xF241d57C6DebAe225c0F2e6eA1529373C9A9C9fB`
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
|
||||
/// @inheritdoc IEntrypoint
|
||||
AssociationSetData[] public associationSets;
|
||||
|
||||
/// @inheritdoc IEntrypoint
|
||||
mapping(uint256 _precommitment => bool _used) public usedPrecommitments;
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
INITIALIZATION
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
@@ -317,6 +320,11 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
|
||||
IPrivacyPool _pool = _config.pool;
|
||||
if (address(_pool) == address(0)) revert PoolNotFound();
|
||||
|
||||
// Check if the `_precommitment` has already been used
|
||||
if (usedPrecommitments[_precommitment]) revert PrecommitmentAlreadyUsed();
|
||||
// Mark it as used
|
||||
usedPrecommitments[_precommitment] = true;
|
||||
|
||||
// Check minimum deposit amount
|
||||
if (_value < _config.minimumDepositAmount) revert MinimumDepositAmount();
|
||||
|
||||
|
||||
@@ -232,6 +232,11 @@ interface IEntrypoint {
|
||||
*/
|
||||
error NativeAssetNotAccepted();
|
||||
|
||||
/**
|
||||
* @notice Thrown when trying to deposit using a precommitment that has already been used by another deposit
|
||||
*/
|
||||
error PrecommitmentAlreadyUsed();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////
|
||||
LOGIC
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
@@ -376,4 +381,11 @@ interface IEntrypoint {
|
||||
* @return _root The ASP root at the index
|
||||
*/
|
||||
function rootByIndex(uint256 _index) external view returns (uint256 _root);
|
||||
|
||||
/**
|
||||
* @notice Returns a boolean indicating if the precommitment has been used
|
||||
* @param _precommitment The precommitment hash
|
||||
* @return _used The usage status
|
||||
*/
|
||||
function usedPrecommitments(uint256 _precommitment) external view returns (bool _used);
|
||||
}
|
||||
|
||||
80
packages/contracts/test/helper/CalculateRoot.mjs
Normal file
80
packages/contracts/test/helper/CalculateRoot.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
import { LeanIMT } from "@zk-kit/lean-imt";
|
||||
import { poseidon } from "maci-crypto/build/ts/hashing.js";
|
||||
import * as fs from "fs";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { encodeAbiParameters } from "viem";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Get CSV file path from command-line argument
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 1) {
|
||||
console.error("Usage: CalculateRoot.mjs <csvFilePath>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Resolve CSV file path relative to project root
|
||||
const projectRoot = resolve(__dirname, '../..');
|
||||
const csvFilePath = resolve(projectRoot, args[0]);
|
||||
|
||||
// Read and parse the CSV data
|
||||
const csvData = fs.readFileSync(csvFilePath, "utf8")
|
||||
.split("\n")
|
||||
.slice(1) // Skip header row
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map((line) => {
|
||||
const parts = line.split(',').map(part => part.trim().replace(/"/g, ''));
|
||||
if (parts.length < 4) { // Basic validation for enough parts
|
||||
console.warn(`Skipping malformed CSV line: ${line}`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return {
|
||||
id: parseInt(parts[0], 10),
|
||||
root: BigInt(parts[1]),
|
||||
index: parseInt(parts[2], 10),
|
||||
leaf: BigInt(parts[3]),
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn(`Skipping line due to parsing error (id: ${parts[0]}, leaf: ${parts[3]}, root: ${parts[1]}): ${line} - Error: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(record => record !== null) // Filter out nulls from malformed lines
|
||||
.sort((a, b) => a.index - b.index); // Sort by index ascending
|
||||
|
||||
if (csvData.length === 0) {
|
||||
console.error("Error: No valid data found in CSV file or CSV format is incorrect after parsing.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Initialize LeanIMT
|
||||
const tree = new LeanIMT((a, b) => poseidon([a, b]));
|
||||
|
||||
|
||||
let errorsFound = 0;
|
||||
for (let i = 0; i < csvData.length; i++) {
|
||||
const record = csvData[i];
|
||||
|
||||
tree.insert(record.leaf);
|
||||
const calculatedRoot = tree.root;
|
||||
|
||||
if (calculatedRoot !== record.root) {
|
||||
errorsFound++;
|
||||
console.error(`MISMATCH found for leaf at index ${record.index} (CSV id: ${record.id}):`);
|
||||
console.error(` Leaf inserted: ${record.leaf}`);
|
||||
console.error(` Calculated Root: ${calculatedRoot}`);
|
||||
console.error(` Expected Root (from CSV): ${record.root}`);
|
||||
console.error(` Tree size after insertion: ${tree.size}`);
|
||||
console.error('--');
|
||||
} else {
|
||||
// console.log(`Index ${record.index} (CSV id: ${record.id}): Root matches (${calculatedRoot})`);
|
||||
}
|
||||
}
|
||||
|
||||
const encodedRoot = encodeAbiParameters([{ type: "uint256" }], [tree.root]);
|
||||
process.stdout.write(encodedRoot);
|
||||
|
||||
86
packages/contracts/test/helper/MerkleProofFromFile.mjs
Normal file
86
packages/contracts/test/helper/MerkleProofFromFile.mjs
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
import { encodeAbiParameters } from "viem";
|
||||
import { generateMerkleProof } from "@0xbow/privacy-pools-core-sdk";
|
||||
import fs from "fs";
|
||||
|
||||
// Get CSV file path and leaf from command-line arguments
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
process.stderr.write("Usage: MerkleProofFromFile.mjs <leavesFile> <leaf>\n");
|
||||
process.exit(1);
|
||||
}
|
||||
const leavesFile = args[0];
|
||||
const leaf = BigInt(args[1]);
|
||||
|
||||
// Read and parse the CSV data (skip header, use 4th column as leaf, sort by index)
|
||||
const csvData = fs.readFileSync(leavesFile, "utf8")
|
||||
.split("\n")
|
||||
.slice(1) // Skip header row
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map((line) => {
|
||||
const parts = line.split(',').map(part => part.trim().replace(/"/g, ''));
|
||||
if (parts.length < 4) return null;
|
||||
try {
|
||||
return {
|
||||
index: parseInt(parts[2], 10),
|
||||
leaf: BigInt(parts[3]),
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(record => record !== null)
|
||||
.sort((a, b) => a.index - b.index);
|
||||
|
||||
let leaves = csvData.map(record => record.leaf);
|
||||
leaves.push(leaf);
|
||||
|
||||
// Wrap the generateMerkleProof call with stdout redirection
|
||||
function withSilentStdout(fn) {
|
||||
const originalStdoutWrite = process.stdout.write;
|
||||
const originalStderrWrite = process.stderr.write;
|
||||
|
||||
return async (...args) => {
|
||||
process.stdout.write = () => true;
|
||||
process.stderr.write = () => true;
|
||||
|
||||
try {
|
||||
const result = await fn(...args);
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
process.stderr.write = originalStderrWrite;
|
||||
return result;
|
||||
} catch (error) {
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
process.stderr.write = originalStderrWrite;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const silentGenerateProof = withSilentStdout(() =>
|
||||
generateMerkleProof(leaves, leaf),
|
||||
);
|
||||
|
||||
const proof = await silentGenerateProof();
|
||||
proof.index = Object.is(proof.index, NaN) ? 0 : proof.index;
|
||||
|
||||
const encodedProof = encodeAbiParameters(
|
||||
[
|
||||
{ name: "root", type: "uint256" },
|
||||
{ name: "index", type: "uint256" },
|
||||
{ name: "siblings", type: "uint256[]" },
|
||||
],
|
||||
[proof.root, proof.index, proof.siblings],
|
||||
);
|
||||
|
||||
process.stdout.write(encodedProof);
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(() => process.exit(1));
|
||||
@@ -15,8 +15,8 @@ import {WithdrawalVerifier} from 'contracts/verifiers/WithdrawalVerifier.sol';
|
||||
import {ERC1967Proxy} from '@oz/proxy/ERC1967/ERC1967Proxy.sol';
|
||||
import {UnsafeUpgrades} from '@upgrades/Upgrades.sol';
|
||||
|
||||
import {IntegrationUtils} from './Utils.sol';
|
||||
import {IERC20} from '@oz/interfaces/IERC20.sol';
|
||||
import {Test} from 'forge-std/Test.sol';
|
||||
|
||||
import {ProofLib} from 'contracts/lib/ProofLib.sol';
|
||||
import {InternalLeanIMT, LeanIMTData} from 'lean-imt/InternalLeanIMT.sol';
|
||||
@@ -28,56 +28,9 @@ import {PoseidonT4} from 'poseidon/PoseidonT4.sol';
|
||||
import {ICreateX} from 'interfaces/external/ICreateX.sol';
|
||||
import {Constants} from 'test/helper/Constants.sol';
|
||||
|
||||
contract IntegrationBase is Test {
|
||||
contract IntegrationBase is IntegrationUtils {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
|
||||
error WithdrawalProofGenerationFailed();
|
||||
error RagequitProofGenerationFailed();
|
||||
error MerkleProofGenerationFailed();
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
STRUCTS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
struct Commitment {
|
||||
uint256 hash;
|
||||
uint256 label;
|
||||
uint256 value;
|
||||
uint256 precommitment;
|
||||
uint256 nullifier;
|
||||
uint256 secret;
|
||||
IERC20 asset;
|
||||
}
|
||||
|
||||
struct DepositParams {
|
||||
address depositor;
|
||||
IERC20 asset;
|
||||
uint256 amount;
|
||||
string nullifier;
|
||||
string secret;
|
||||
}
|
||||
|
||||
struct WithdrawalParams {
|
||||
uint256 withdrawnAmount;
|
||||
string newNullifier;
|
||||
string newSecret;
|
||||
address recipient;
|
||||
Commitment commitment;
|
||||
bytes4 revertReason;
|
||||
}
|
||||
|
||||
struct WithdrawalProofParams {
|
||||
uint256 existingCommitment;
|
||||
uint256 withdrawnValue;
|
||||
uint256 context;
|
||||
uint256 label;
|
||||
uint256 existingValue;
|
||||
uint256 existingNullifier;
|
||||
uint256 existingSecret;
|
||||
uint256 newNullifier;
|
||||
uint256 newSecret;
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
STATE VARIABLES
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
@@ -100,12 +53,6 @@ contract IntegrationBase is Test {
|
||||
IERC20 internal constant _DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
|
||||
IERC20 internal _ETH = IERC20(Constants.NATIVE_ASSET);
|
||||
|
||||
// Mirrored Merkle Trees
|
||||
LeanIMTData internal _shadowMerkleTree;
|
||||
uint256[] internal _merkleLeaves;
|
||||
LeanIMTData internal _shadowASPMerkleTree;
|
||||
uint256[] internal _aspLeaves;
|
||||
|
||||
// Snark Scalar Field
|
||||
uint256 public constant SNARK_SCALAR_FIELD =
|
||||
21_888_242_871_839_275_222_246_405_745_257_275_088_548_364_400_416_034_343_698_204_186_575_808_495_617;
|
||||
@@ -429,168 +376,4 @@ contract IntegrationBase is Test {
|
||||
_balance(address(_pool), _commitment.asset), _poolInitialBalance - _commitment.value, 'Pool balance mismatch'
|
||||
);
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
MERKLE TREE OPERATIONS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _insertIntoShadowMerkleTree(uint256 _leaf) private {
|
||||
_shadowMerkleTree._insert(_leaf);
|
||||
_merkleLeaves.push(_leaf);
|
||||
}
|
||||
|
||||
function _insertIntoShadowASPMerkleTree(uint256 _leaf) private {
|
||||
_shadowASPMerkleTree._insert(_leaf);
|
||||
_aspLeaves.push(_leaf);
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
PROOF GENERATION
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _generateRagequitProof(
|
||||
uint256 _value,
|
||||
uint256 _label,
|
||||
uint256 _nullifier,
|
||||
uint256 _secret
|
||||
) internal returns (ProofLib.RagequitProof memory _proof) {
|
||||
// Generate real proof using the helper script
|
||||
string[] memory _inputs = new string[](5);
|
||||
_inputs[0] = vm.toString(_value);
|
||||
_inputs[1] = vm.toString(_label);
|
||||
_inputs[2] = vm.toString(_nullifier);
|
||||
_inputs[3] = vm.toString(_secret);
|
||||
|
||||
// Call the ProofGenerator script using ts-node
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/RagequitProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
if (_proofData.length == 0) {
|
||||
revert RagequitProofGenerationFailed();
|
||||
}
|
||||
|
||||
// Decode the ABI-encoded proof directly
|
||||
_proof = abi.decode(_proofData, (ProofLib.RagequitProof));
|
||||
}
|
||||
|
||||
function _generateWithdrawalProof(WithdrawalProofParams memory _params)
|
||||
internal
|
||||
returns (ProofLib.WithdrawProof memory _proof)
|
||||
{
|
||||
// Generate state merkle proof
|
||||
bytes memory _stateMerkleProof = _generateMerkleProof(_merkleLeaves, _params.existingCommitment);
|
||||
// Generate ASP merkle proof
|
||||
bytes memory _aspMerkleProof = _generateMerkleProof(_aspLeaves, _params.label);
|
||||
|
||||
if (_aspMerkleProof.length == 0 || _stateMerkleProof.length == 0) {
|
||||
revert MerkleProofGenerationFailed();
|
||||
}
|
||||
|
||||
string[] memory _inputs = new string[](12);
|
||||
_inputs[0] = vm.toString(_params.existingValue);
|
||||
_inputs[1] = vm.toString(_params.label);
|
||||
_inputs[2] = vm.toString(_params.existingNullifier);
|
||||
_inputs[3] = vm.toString(_params.existingSecret);
|
||||
_inputs[4] = vm.toString(_params.newNullifier);
|
||||
_inputs[5] = vm.toString(_params.newSecret);
|
||||
_inputs[6] = vm.toString(_params.withdrawnValue);
|
||||
_inputs[7] = vm.toString(_params.context);
|
||||
_inputs[8] = vm.toString(_stateMerkleProof);
|
||||
_inputs[9] = vm.toString(_shadowMerkleTree.depth);
|
||||
_inputs[10] = vm.toString(_aspMerkleProof);
|
||||
_inputs[11] = vm.toString(_shadowASPMerkleTree.depth);
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
if (_proofData.length == 0) {
|
||||
revert WithdrawalProofGenerationFailed();
|
||||
}
|
||||
|
||||
_proof = abi.decode(_proofData, (ProofLib.WithdrawProof));
|
||||
}
|
||||
|
||||
function _generateMerkleProof(uint256[] storage _leaves, uint256 _leaf) internal returns (bytes memory _proof) {
|
||||
uint256 _leavesAmt = _leaves.length;
|
||||
string[] memory inputs = new string[](_leavesAmt + 1);
|
||||
inputs[0] = vm.toString(_leaf);
|
||||
|
||||
for (uint256 i = 0; i < _leavesAmt; i++) {
|
||||
inputs[i + 1] = vm.toString(_leaves[i]);
|
||||
}
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory scriptArgs = new string[](2);
|
||||
scriptArgs[0] = 'node';
|
||||
scriptArgs[1] = 'test/helper/MerkleProofGenerator.mjs';
|
||||
_proof = vm.ffi(_concat(scriptArgs, inputs));
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
UTILS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _concat(string[] memory _arr1, string[] memory _arr2) internal pure returns (string[] memory) {
|
||||
string[] memory returnArr = new string[](_arr1.length + _arr2.length);
|
||||
uint256 i;
|
||||
for (; i < _arr1.length;) {
|
||||
returnArr[i] = _arr1[i];
|
||||
unchecked {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
uint256 j;
|
||||
for (; j < _arr2.length;) {
|
||||
returnArr[i + j] = _arr2[j];
|
||||
unchecked {
|
||||
++j;
|
||||
}
|
||||
}
|
||||
return returnArr;
|
||||
}
|
||||
|
||||
function _deal(address _account, IERC20 _asset, uint256 _amount) private {
|
||||
if (_asset == IERC20(Constants.NATIVE_ASSET)) {
|
||||
deal(_account, _amount);
|
||||
} else {
|
||||
deal(address(_asset), _account, _amount);
|
||||
}
|
||||
}
|
||||
|
||||
function _balance(address _account, IERC20 _asset) private view returns (uint256 _bal) {
|
||||
if (_asset == IERC20(Constants.NATIVE_ASSET)) {
|
||||
_bal = _account.balance;
|
||||
} else {
|
||||
_bal = _asset.balanceOf(_account);
|
||||
}
|
||||
}
|
||||
|
||||
function _deductFee(uint256 _amount, uint256 _feeBps) private pure returns (uint256 _amountAfterFee) {
|
||||
_amountAfterFee = _amount - (_amount * _feeBps) / 10_000;
|
||||
}
|
||||
|
||||
function _hashNullifier(uint256 _nullifier) private pure returns (uint256 _nullifierHash) {
|
||||
_nullifierHash = PoseidonT2.hash([_nullifier]);
|
||||
}
|
||||
|
||||
function _hashPrecommitment(uint256 _nullifier, uint256 _secret) private pure returns (uint256 _precommitment) {
|
||||
_precommitment = PoseidonT3.hash([_nullifier, _secret]);
|
||||
}
|
||||
|
||||
function _hashCommitment(
|
||||
uint256 _amount,
|
||||
uint256 _label,
|
||||
uint256 _precommitment
|
||||
) private pure returns (uint256 _commitmentHash) {
|
||||
_commitmentHash = PoseidonT4.hash([_amount, _label, _precommitment]);
|
||||
}
|
||||
|
||||
function _genSecretBySeed(string memory _seed) internal pure returns (uint256 _secret) {
|
||||
_secret = uint256(keccak256(bytes(_seed))) % Constants.SNARK_SCALAR_FIELD;
|
||||
}
|
||||
}
|
||||
|
||||
256
packages/contracts/test/integration/Utils.sol
Normal file
256
packages/contracts/test/integration/Utils.sol
Normal file
@@ -0,0 +1,256 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {ProofLib} from 'contracts/lib/ProofLib.sol';
|
||||
import {Constants} from 'test/helper/Constants.sol';
|
||||
|
||||
import {IERC20} from '@oz/interfaces/IERC20.sol';
|
||||
import {Test} from 'forge-std/Test.sol';
|
||||
import {InternalLeanIMT, LeanIMTData} from 'lean-imt/InternalLeanIMT.sol';
|
||||
|
||||
import {PoseidonT2} from 'poseidon/PoseidonT2.sol';
|
||||
import {PoseidonT3} from 'poseidon/PoseidonT3.sol';
|
||||
import {PoseidonT4} from 'poseidon/PoseidonT4.sol';
|
||||
|
||||
contract IntegrationUtils is Test {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
STRUCTS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
struct Commitment {
|
||||
uint256 hash;
|
||||
uint256 label;
|
||||
uint256 value;
|
||||
uint256 precommitment;
|
||||
uint256 nullifier;
|
||||
uint256 secret;
|
||||
IERC20 asset;
|
||||
}
|
||||
|
||||
struct DepositParams {
|
||||
address depositor;
|
||||
IERC20 asset;
|
||||
uint256 amount;
|
||||
string nullifier;
|
||||
string secret;
|
||||
}
|
||||
|
||||
struct WithdrawalParams {
|
||||
uint256 withdrawnAmount;
|
||||
string newNullifier;
|
||||
string newSecret;
|
||||
address recipient;
|
||||
Commitment commitment;
|
||||
bytes4 revertReason;
|
||||
}
|
||||
|
||||
struct WithdrawalProofParams {
|
||||
uint256 existingCommitment;
|
||||
uint256 withdrawnValue;
|
||||
uint256 context;
|
||||
uint256 label;
|
||||
uint256 existingValue;
|
||||
uint256 existingNullifier;
|
||||
uint256 existingSecret;
|
||||
uint256 newNullifier;
|
||||
uint256 newSecret;
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
ERRORS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
error WithdrawalProofGenerationFailed();
|
||||
error RagequitProofGenerationFailed();
|
||||
error MerkleProofGenerationFailed();
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
MERKLE TREES
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
LeanIMTData internal _shadowMerkleTree;
|
||||
uint256[] internal _merkleLeaves;
|
||||
LeanIMTData internal _shadowASPMerkleTree;
|
||||
uint256[] internal _aspLeaves;
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
PROOF GENERATION
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _generateRagequitProof(
|
||||
uint256 _value,
|
||||
uint256 _label,
|
||||
uint256 _nullifier,
|
||||
uint256 _secret
|
||||
) internal returns (ProofLib.RagequitProof memory _proof) {
|
||||
// Generate real proof using the helper script
|
||||
string[] memory _inputs = new string[](5);
|
||||
_inputs[0] = vm.toString(_value);
|
||||
_inputs[1] = vm.toString(_label);
|
||||
_inputs[2] = vm.toString(_nullifier);
|
||||
_inputs[3] = vm.toString(_secret);
|
||||
|
||||
// Call the ProofGenerator script using ts-node
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/RagequitProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
if (_proofData.length == 0) {
|
||||
revert RagequitProofGenerationFailed();
|
||||
}
|
||||
|
||||
// Decode the ABI-encoded proof directly
|
||||
_proof = abi.decode(_proofData, (ProofLib.RagequitProof));
|
||||
}
|
||||
|
||||
function _generateWithdrawalProof(WithdrawalProofParams memory _params)
|
||||
internal
|
||||
returns (ProofLib.WithdrawProof memory _proof)
|
||||
{
|
||||
// Generate state merkle proof
|
||||
bytes memory _stateMerkleProof = _generateMerkleProof(_merkleLeaves, _params.existingCommitment);
|
||||
// Generate ASP merkle proof
|
||||
bytes memory _aspMerkleProof = _generateMerkleProof(_aspLeaves, _params.label);
|
||||
|
||||
if (_aspMerkleProof.length == 0 || _stateMerkleProof.length == 0) {
|
||||
revert MerkleProofGenerationFailed();
|
||||
}
|
||||
|
||||
string[] memory _inputs = new string[](12);
|
||||
_inputs[0] = vm.toString(_params.existingValue);
|
||||
_inputs[1] = vm.toString(_params.label);
|
||||
_inputs[2] = vm.toString(_params.existingNullifier);
|
||||
_inputs[3] = vm.toString(_params.existingSecret);
|
||||
_inputs[4] = vm.toString(_params.newNullifier);
|
||||
_inputs[5] = vm.toString(_params.newSecret);
|
||||
_inputs[6] = vm.toString(_params.withdrawnValue);
|
||||
_inputs[7] = vm.toString(_params.context);
|
||||
_inputs[8] = vm.toString(_stateMerkleProof);
|
||||
_inputs[9] = vm.toString(_shadowMerkleTree.depth);
|
||||
_inputs[10] = vm.toString(_aspMerkleProof);
|
||||
_inputs[11] = vm.toString(_shadowASPMerkleTree.depth);
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
if (_proofData.length == 0) {
|
||||
revert WithdrawalProofGenerationFailed();
|
||||
}
|
||||
|
||||
_proof = abi.decode(_proofData, (ProofLib.WithdrawProof));
|
||||
}
|
||||
|
||||
function _generateMerkleProof(uint256[] storage _leaves, uint256 _leaf) internal returns (bytes memory _proof) {
|
||||
uint256 _leavesAmt = _leaves.length;
|
||||
string[] memory inputs = new string[](_leavesAmt + 1);
|
||||
inputs[0] = vm.toString(_leaf);
|
||||
|
||||
for (uint256 i = 0; i < _leavesAmt; i++) {
|
||||
inputs[i + 1] = vm.toString(_leaves[i]);
|
||||
}
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory scriptArgs = new string[](2);
|
||||
scriptArgs[0] = 'node';
|
||||
scriptArgs[1] = 'test/helper/MerkleProofGenerator.mjs';
|
||||
_proof = vm.ffi(_concat(scriptArgs, inputs));
|
||||
}
|
||||
|
||||
function _generateMerkleProofMemory(uint256[] memory _leaves, uint256 _leaf) internal returns (bytes memory _proof) {
|
||||
uint256 _leavesAmt = _leaves.length;
|
||||
string[] memory inputs = new string[](_leavesAmt + 1);
|
||||
inputs[0] = vm.toString(_leaf);
|
||||
for (uint256 i = 0; i < _leavesAmt; i++) {
|
||||
inputs[i + 1] = vm.toString(_leaves[i]);
|
||||
}
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory scriptArgs = new string[](2);
|
||||
scriptArgs[0] = 'node';
|
||||
scriptArgs[1] = 'test/helper/MerkleProofGenerator.mjs';
|
||||
_proof = vm.ffi(_concat(scriptArgs, inputs));
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
UTILS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _deal(address _account, IERC20 _asset, uint256 _amount) internal {
|
||||
if (_asset == IERC20(Constants.NATIVE_ASSET)) {
|
||||
deal(_account, _amount);
|
||||
} else {
|
||||
deal(address(_asset), _account, _amount);
|
||||
}
|
||||
}
|
||||
|
||||
function _balance(address _account, IERC20 _asset) internal view returns (uint256 _bal) {
|
||||
if (_asset == IERC20(Constants.NATIVE_ASSET)) {
|
||||
_bal = _account.balance;
|
||||
} else {
|
||||
_bal = _asset.balanceOf(_account);
|
||||
}
|
||||
}
|
||||
|
||||
function _concat(string[] memory _arr1, string[] memory _arr2) internal pure returns (string[] memory) {
|
||||
string[] memory returnArr = new string[](_arr1.length + _arr2.length);
|
||||
uint256 i;
|
||||
for (; i < _arr1.length;) {
|
||||
returnArr[i] = _arr1[i];
|
||||
unchecked {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
uint256 j;
|
||||
for (; j < _arr2.length;) {
|
||||
returnArr[i + j] = _arr2[j];
|
||||
unchecked {
|
||||
++j;
|
||||
}
|
||||
}
|
||||
return returnArr;
|
||||
}
|
||||
|
||||
function _deductFee(uint256 _amount, uint256 _feeBPS) internal pure returns (uint256 _afterFees) {
|
||||
_afterFees = _amount - ((_amount * _feeBPS) / 10_000);
|
||||
}
|
||||
|
||||
function _hashNullifier(uint256 _nullifier) internal pure returns (uint256 _nullifierHash) {
|
||||
_nullifierHash = PoseidonT2.hash([_nullifier]);
|
||||
}
|
||||
|
||||
function _hashPrecommitment(uint256 _nullifier, uint256 _secret) internal pure returns (uint256 _precommitment) {
|
||||
_precommitment = PoseidonT3.hash([_nullifier, _secret]);
|
||||
}
|
||||
|
||||
function _hashCommitment(
|
||||
uint256 _amount,
|
||||
uint256 _label,
|
||||
uint256 _precommitment
|
||||
) internal pure returns (uint256 _commitmentHash) {
|
||||
_commitmentHash = PoseidonT4.hash([_amount, _label, _precommitment]);
|
||||
}
|
||||
|
||||
function _genSecretBySeed(string memory _seed) internal pure returns (uint256 _secret) {
|
||||
_secret = uint256(keccak256(bytes(_seed))) % Constants.SNARK_SCALAR_FIELD;
|
||||
}
|
||||
|
||||
/*///////////////////////////////////////////////////////////////
|
||||
MERKLE TREE OPERATIONS
|
||||
//////////////////////////////////////////////////////////////*/
|
||||
|
||||
function _insertIntoShadowMerkleTree(uint256 _leaf) internal {
|
||||
_shadowMerkleTree._insert(_leaf);
|
||||
_merkleLeaves.push(_leaf);
|
||||
}
|
||||
|
||||
function _insertIntoShadowASPMerkleTree(uint256 _leaf) internal {
|
||||
_shadowASPMerkleTree._insert(_leaf);
|
||||
_aspLeaves.push(_leaf);
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,19 @@ import {Constants, IPrivacyPool, IPrivacyPool, ProofLib} from 'contracts/Privacy
|
||||
import {IEntrypoint} from 'interfaces/IEntrypoint.sol';
|
||||
|
||||
contract HandlersEntrypoint is Setup {
|
||||
function handler_deposit(uint256 _amount) public {
|
||||
function handler_deposit(uint256 _amount, uint256 _precommitment) public {
|
||||
_amount = clampLt(_amount, type(uint128).max / FEE_DENOMINATOR);
|
||||
|
||||
uint256 _poolBalanceBefore = token.balanceOf(address(tokenPool));
|
||||
uint256 _entrypointBalanceBefore = token.balanceOf(address(entrypoint));
|
||||
|
||||
vm.assume(entrypoint.usedPrecommitments(_precommitment) == false);
|
||||
|
||||
token.transfer(address(currentActor()), _amount);
|
||||
(bool success, bytes memory result) = currentActor().call(
|
||||
address(entrypoint), 0, abi.encodeWithSignature('deposit(address,uint256,uint256)', token, _amount, 1)
|
||||
address(entrypoint),
|
||||
0,
|
||||
abi.encodeWithSignature('deposit(address,uint256,uint256)', token, _amount, _precommitment)
|
||||
);
|
||||
|
||||
if (success) {
|
||||
|
||||
@@ -98,6 +98,10 @@ contract EntrypointForTest is Entrypoint {
|
||||
assetConfig[_asset].maxRelayFeeBPS = _maxRelayFeeBPS;
|
||||
}
|
||||
|
||||
function mockUsedPrecommitment(uint256 _precommitment) external {
|
||||
usedPrecommitments[_precommitment] = true;
|
||||
}
|
||||
|
||||
bytes32 public constant OWNER_ROLE = keccak256('OWNER_ROLE');
|
||||
|
||||
bytes32 public constant ASP_POSTMAN = keccak256('ASP_POSTMAN');
|
||||
@@ -227,6 +231,8 @@ contract UnitRootUpdate is UnitEntrypoint {
|
||||
uint256 _length = bytes(_ipfsCID).length;
|
||||
vm.assume(_length >= 32 && _length <= 64);
|
||||
|
||||
_timestamp = bound(_timestamp, 1, type(uint64).max - 1);
|
||||
|
||||
vm.warp(_timestamp);
|
||||
|
||||
vm.expectEmit(address(_entrypoint));
|
||||
@@ -332,7 +338,7 @@ contract UnitDeposit is UnitEntrypoint {
|
||||
uint256 _depositorBalanceBefore = _depositor.balance;
|
||||
|
||||
vm.expectEmit(address(_entrypoint));
|
||||
emit IEntrypoint.Deposited(_depositor, _pool, _commitment, _amountAfterFees);
|
||||
emit IEntrypoint.Deposited(_depositor, IPrivacyPool(_params.pool), _commitment, _amountAfterFees);
|
||||
|
||||
vm.prank(_depositor);
|
||||
_entrypoint.deposit{value: _amount}(_precommitment);
|
||||
@@ -375,6 +381,7 @@ contract UnitDeposit is UnitEntrypoint {
|
||||
}
|
||||
|
||||
function test_DepositETHWhenPoolNotFound(address _depositor, uint256 _amount, uint256 _precommitment) external {
|
||||
vm.assume(_depositor != address(0));
|
||||
vm.deal(_depositor, _amount);
|
||||
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.PoolNotFound.selector));
|
||||
vm.prank(_depositor);
|
||||
@@ -391,6 +398,7 @@ contract UnitDeposit is UnitEntrypoint {
|
||||
uint256 _commitment,
|
||||
PoolParams memory _params
|
||||
) external givenPoolExists(_params) {
|
||||
_assumeFuzzable(_depositor);
|
||||
vm.assume(_depositor != address(0));
|
||||
|
||||
// Can't be too big, otherwise overflows
|
||||
@@ -449,7 +457,6 @@ contract UnitDeposit is UnitEntrypoint {
|
||||
) external {
|
||||
_assumeFuzzable(_asset);
|
||||
vm.assume(_depositor != address(0));
|
||||
vm.assume(_asset != address(0));
|
||||
vm.assume(_asset != _ETH);
|
||||
|
||||
_mockAndExpect(
|
||||
@@ -462,6 +469,68 @@ contract UnitDeposit is UnitEntrypoint {
|
||||
vm.prank(_depositor);
|
||||
_entrypoint.deposit(IERC20(_asset), _amount, _precommitment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the Entrypoint reverts when the precommitment has already been used for ETH deposits
|
||||
*/
|
||||
function test_DepositETHWhenPrecommitmentAlreadyUsed(
|
||||
address _depositor,
|
||||
uint256 _amount,
|
||||
uint256 _precommitment,
|
||||
PoolParams memory _params
|
||||
)
|
||||
external
|
||||
givenPoolExists(
|
||||
PoolParams({
|
||||
pool: _params.pool,
|
||||
asset: _ETH,
|
||||
minDeposit: _params.minDeposit,
|
||||
vettingFeeBPS: _params.vettingFeeBPS,
|
||||
maxRelayFeeBPS: 500 // Default to 5%
|
||||
})
|
||||
)
|
||||
{
|
||||
_assumeFuzzable(_depositor);
|
||||
vm.assume(_depositor != address(_entrypoint));
|
||||
|
||||
(, uint256 _minDeposit,,) = _entrypoint.assetConfig(IERC20(_ETH));
|
||||
_amount = bound(_amount, _minDeposit, 1e30);
|
||||
deal(_depositor, _amount);
|
||||
|
||||
// Mark the precommitment as used
|
||||
_entrypoint.mockUsedPrecommitment(_precommitment);
|
||||
|
||||
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.PrecommitmentAlreadyUsed.selector));
|
||||
vm.prank(_depositor);
|
||||
_entrypoint.deposit{value: _amount}(_precommitment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the Entrypoint reverts when the precommitment has already been used for ERC20 deposits
|
||||
*/
|
||||
function test_DepositERC20WhenPrecommitmentAlreadyUsed(
|
||||
address _depositor,
|
||||
uint256 _amount,
|
||||
uint256 _precommitment,
|
||||
PoolParams memory _params
|
||||
) external givenPoolExists(_params) {
|
||||
vm.assume(_depositor != address(0));
|
||||
|
||||
_amount = bound(_amount, _params.minDeposit, 1e30);
|
||||
|
||||
// Mark the precommitment as used
|
||||
_entrypoint.mockUsedPrecommitment(_precommitment);
|
||||
|
||||
_mockAndExpect(
|
||||
_params.asset,
|
||||
abi.encodeWithSignature('transferFrom(address,address,uint256)', _depositor, address(_entrypoint), _amount),
|
||||
abi.encode(true)
|
||||
);
|
||||
|
||||
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.PrecommitmentAlreadyUsed.selector));
|
||||
vm.prank(_depositor);
|
||||
_entrypoint.deposit(IERC20(_params.asset), _amount, _precommitment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,8 +33,10 @@ Entrypoint::deposit (ETH)
|
||||
│ │ │ ├── It forwards ETH to pool
|
||||
│ │ │ ├── It maintains contract balance
|
||||
│ │ │ └── It emits Deposited event
|
||||
│ │ └── When value below minimum
|
||||
│ │ └── It reverts with MinimumDepositAmount
|
||||
│ │ ├── When value below minimum
|
||||
│ │ │ └── It reverts with MinimumDepositAmount
|
||||
│ │ └── When precommitment already used
|
||||
│ │ └── It reverts with PrecommitmentAlreadyUsed
|
||||
│ └── When pool not found
|
||||
│ └── It reverts with PoolNotFound
|
||||
└── When reentrant call
|
||||
@@ -48,8 +50,10 @@ Entrypoint::deposit (ERC20)
|
||||
│ │ │ ├── It deducts correct fees
|
||||
│ │ │ ├── It deposits to pool
|
||||
│ │ │ └── It emits Deposited event
|
||||
│ │ └── When value below minimum
|
||||
│ │ └── It reverts with MinimumDepositAmount
|
||||
│ │ ├── When value below minimum
|
||||
│ │ │ └── It reverts with MinimumDepositAmount
|
||||
│ │ └── When precommitment already used
|
||||
│ │ └── It reverts with PrecommitmentAlreadyUsed
|
||||
│ └── When pool not found
|
||||
│ └── It reverts with PoolNotFound
|
||||
└── When reentrant call
|
||||
|
||||
720
packages/contracts/test/upgrades/EntrypointUpgrade.t.sol
Normal file
720
packages/contracts/test/upgrades/EntrypointUpgrade.t.sol
Normal file
@@ -0,0 +1,720 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {IntegrationUtils} from '../integration/Utils.sol';
|
||||
import {IERC1967} from '@oz/interfaces/IERC1967.sol';
|
||||
import {Test} from 'forge-std/Test.sol';
|
||||
|
||||
import {IERC20} from '@oz/interfaces/IERC20.sol';
|
||||
import {Initializable} from '@oz/proxy/utils/Initializable.sol';
|
||||
import {Constants} from 'contracts/lib/Constants.sol';
|
||||
|
||||
import {Entrypoint, IEntrypoint} from 'contracts/Entrypoint.sol';
|
||||
import {PrivacyPoolComplex} from 'contracts/implementations/PrivacyPoolComplex.sol';
|
||||
import {ProofLib} from 'contracts/lib/ProofLib.sol';
|
||||
import {IPrivacyPool} from 'interfaces/IPrivacyPool.sol';
|
||||
|
||||
contract MainnetEnvironment {
|
||||
/// @notice Current implementation address
|
||||
address public implementationV1 = 0xdD8aA0560a08E39C0b3A84BBa356Bc025AfbD4C1;
|
||||
/// @notice Entrypoint ERC1967Proxy address
|
||||
Entrypoint public proxy = Entrypoint(payable(0x6818809EefCe719E480a7526D76bD3e561526b46));
|
||||
|
||||
/// @notice ETH Privacy Pool address
|
||||
IPrivacyPool public ethPool = IPrivacyPool(0xF241d57C6DebAe225c0F2e6eA1529373C9A9C9fB);
|
||||
|
||||
/// @notice Owner address (SAFE multisig)
|
||||
address public owner = 0xAd7f9A19E2598b6eFE0A25C84FB1c87F81eB7159;
|
||||
/// @notice Postman address
|
||||
address public postman = 0x1f4Fe25Cf802a0605229e0Dc497aAf653E86E187;
|
||||
|
||||
/// @notice Association set index at fork block
|
||||
uint256 internal _associationSetIndex = 20;
|
||||
|
||||
/// @notice Ethereum Mainnet fork block
|
||||
uint256 internal constant _FORK_BLOCK = 22_495_337;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title EntrypointUpgradeIntegration
|
||||
* @notice Integration tests for upgrading the Entrypoint contract on mainnet
|
||||
* @dev This test suite verifies the upgrade process of the Entrypoint contract using UUPS proxy pattern
|
||||
* @dev Tests are run against a forked mainnet environment to ensure compatibility with production state
|
||||
*/
|
||||
contract EntrypointUpgradeIntegration is Test, IntegrationUtils, MainnetEnvironment {
|
||||
/// @notice Entrypoint owner role
|
||||
bytes32 internal constant _OWNER_ROLE = keccak256('OWNER_ROLE');
|
||||
/// @notice Entrypoint postman role
|
||||
bytes32 internal constant _ASP_POSTMAN = keccak256('ASP_POSTMAN');
|
||||
/// @notice Storage slot where the implementation address is located for ERC1967 Proxies
|
||||
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
|
||||
|
||||
bytes32 private constant _INITIALIZABLE_SLOT = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;
|
||||
|
||||
/// @notice Pool configuration tracking
|
||||
IPrivacyPool internal _poolAddressFromConfig;
|
||||
uint256 internal _minimumDepositAmountFromConfig;
|
||||
uint256 internal _vettingFeeBPSFromConfig;
|
||||
uint256 internal _maxRelayFeeBPSFromConfig;
|
||||
uint256 internal _ethPoolScopeFromConfig;
|
||||
uint256 internal _ethPoolScope;
|
||||
|
||||
/// @notice Root state tracking
|
||||
uint256 internal _latestASPRoot;
|
||||
uint256 internal _latestRootByIndex;
|
||||
|
||||
address internal _user = makeAddr('user');
|
||||
address internal _relayer = makeAddr('relayer');
|
||||
address internal _recipient = makeAddr('recipient');
|
||||
|
||||
/// @notice Variables for testing flows, trying to avoid stack-too-deep
|
||||
uint256 internal _value;
|
||||
uint256 internal _label;
|
||||
uint256 internal _precommitment;
|
||||
uint256 internal _nullifier;
|
||||
uint256 internal _secret;
|
||||
uint256 internal _context;
|
||||
ProofLib.WithdrawProof internal _withdrawProof;
|
||||
ProofLib.RagequitProof internal _ragequitProof;
|
||||
|
||||
function setUp() public {
|
||||
// Fork from specific block since that's the tree state we're using
|
||||
vm.createSelectFork(vm.rpcUrl('mainnet'), _FORK_BLOCK);
|
||||
|
||||
// Store current asset configuration previous to upgrade
|
||||
(_poolAddressFromConfig, _minimumDepositAmountFromConfig, _vettingFeeBPSFromConfig, _maxRelayFeeBPSFromConfig) =
|
||||
proxy.assetConfig(IERC20(Constants.NATIVE_ASSET));
|
||||
_ethPoolScope = ethPool.SCOPE();
|
||||
|
||||
// Store root state previous to upgrade
|
||||
_latestASPRoot = proxy.latestRoot();
|
||||
_latestRootByIndex = proxy.rootByIndex(_associationSetIndex);
|
||||
|
||||
// Deploy new Entrypoint implementation
|
||||
Entrypoint _newImplementation = new Entrypoint();
|
||||
|
||||
// Expect event emission with new implementation address
|
||||
vm.expectEmit(address(proxy));
|
||||
emit IERC1967.Upgraded(address(_newImplementation));
|
||||
|
||||
// As owner, upgrade to the new implementation
|
||||
vm.prank(owner);
|
||||
proxy.upgradeToAndCall(address(_newImplementation), '');
|
||||
|
||||
// Check the implementation was successfully updated in the proxy storage
|
||||
bytes32 _implementationAddressRaw = vm.load(address(proxy), _IMPLEMENTATION_SLOT);
|
||||
assertEq(
|
||||
address(uint160(uint256(_implementationAddressRaw))),
|
||||
address(_newImplementation),
|
||||
"Implementation addresses don't match"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the Entrypoint state and configuration is kept the same
|
||||
*/
|
||||
function test_StateIsKept() public {
|
||||
// Check initialization status
|
||||
bytes32 _initializableStorage = vm.load(address(proxy), _INITIALIZABLE_SLOT);
|
||||
uint64 _initializedVersion = uint64(uint256(_initializableStorage));
|
||||
assertEq(_initializedVersion, 1, 'Proxy must be already initialized');
|
||||
|
||||
// Check can't be initialized again
|
||||
vm.expectRevert(Initializable.InvalidInitialization.selector);
|
||||
vm.prank(owner);
|
||||
proxy.initialize(owner, postman);
|
||||
|
||||
// Check owner has kept his role
|
||||
assertTrue(proxy.hasRole(_OWNER_ROLE, owner), 'Owner address must have the owner role');
|
||||
assertTrue(proxy.hasRole(_ASP_POSTMAN, postman), 'Postman address must have the postman role');
|
||||
|
||||
// Fetch current configuration for ETH pool
|
||||
(IPrivacyPool _pool, uint256 _minimumDepositAmount, uint256 _vettingFeeBPS, uint256 _maxRelayFeeBPS) =
|
||||
proxy.assetConfig(IERC20(Constants.NATIVE_ASSET));
|
||||
|
||||
// Check the address for the ETH pool has not changed
|
||||
assertEq(address(_pool), address(_poolAddressFromConfig), 'ETH pool address must be the same');
|
||||
// Check the minimum deposit amount for the ETH pool has not changed
|
||||
assertEq(_minimumDepositAmount, _minimumDepositAmountFromConfig, 'Minimum deposit amount must be the same');
|
||||
// Check the vetting fee for the ETH pool has not changed
|
||||
assertEq(_vettingFeeBPS, _vettingFeeBPSFromConfig, 'Vetting fee BPS must be the same');
|
||||
// Check the max relay fee for the ETH pool has not changed
|
||||
assertEq(_maxRelayFeeBPS, _maxRelayFeeBPSFromConfig, 'Max relay fee BPS must be the same');
|
||||
|
||||
// Check the registered scope for the ETH pool has not changed
|
||||
assertEq(address(_pool), address(proxy.scopeToPool(_ethPoolScope)), 'ETH pool scope must match');
|
||||
|
||||
// Check the latest root has not changed
|
||||
assertEq(proxy.latestRoot(), _latestASPRoot, 'Root must have not changed');
|
||||
// Check the latest root index has not changed
|
||||
assertEq(proxy.rootByIndex(_associationSetIndex), _latestASPRoot, 'Index must have not changed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the Postman can still post roots and they get properly udpated
|
||||
*/
|
||||
function test_UpdateRoot() public {
|
||||
uint256 _newRoot = uint256(keccak256('some_root'));
|
||||
|
||||
// Push some random root as postman
|
||||
vm.prank(postman);
|
||||
proxy.updateRoot(_newRoot, 'ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid');
|
||||
|
||||
// Check lates root and latest index were updated correctly
|
||||
assertEq(proxy.latestRoot(), _newRoot, 'ASP root must have been updated');
|
||||
assertEq(proxy.rootByIndex(_associationSetIndex + 1), _newRoot, 'ASP root index must have been updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that users can deposit and the balances get updated accordingly
|
||||
*/
|
||||
function test_ETHDeposit() public {
|
||||
uint256 _depositAmount = 10 ether;
|
||||
|
||||
// Calculate deposited amount after configured fees
|
||||
_value = _deductFee(_depositAmount, _vettingFeeBPSFromConfig);
|
||||
uint256 _fees = _depositAmount - _value;
|
||||
|
||||
// Deal user
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
// Fetch previous balances
|
||||
uint256 _entrypointBalanceBefore = address(proxy).balance;
|
||||
uint256 _poolBalanceBefore = address(ethPool).balance;
|
||||
|
||||
// Expect `deposit` call to ETH pool
|
||||
vm.expectCall(
|
||||
address(ethPool),
|
||||
_value,
|
||||
abi.encodeWithSelector(IPrivacyPool.deposit.selector, _user, _value, uint256(keccak256('precommitment')))
|
||||
);
|
||||
|
||||
// Deposit
|
||||
vm.prank(_user);
|
||||
proxy.deposit{value: _depositAmount}(uint256(keccak256('precommitment')));
|
||||
|
||||
// Check balances were updated correctly
|
||||
assertEq(_entrypointBalanceBefore + _fees, address(proxy).balance, 'Entrypoint balance mismatch');
|
||||
assertEq(_poolBalanceBefore + _value, address(ethPool).balance, 'Pool balance mismatch');
|
||||
|
||||
// Can't reuse same precommitment
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
vm.expectRevert(IEntrypoint.PrecommitmentAlreadyUsed.selector);
|
||||
vm.prank(_user);
|
||||
proxy.deposit{value: _depositAmount}(uint256(keccak256('precommitment')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the owner can register a new pool and wind it down
|
||||
*/
|
||||
function test_RegisterNewPool() public {
|
||||
address _raiToken = 0x03ab458634910AaD20eF5f1C8ee96F1D6ac54919;
|
||||
|
||||
// Deploy new RAI pool
|
||||
PrivacyPoolComplex _raiPool = new PrivacyPoolComplex(
|
||||
address(proxy), address(ethPool.WITHDRAWAL_VERIFIER()), address(ethPool.RAGEQUIT_VERIFIER()), _raiToken
|
||||
);
|
||||
uint256 _poolScope = _raiPool.SCOPE();
|
||||
|
||||
// Register pool as owner
|
||||
vm.prank(owner);
|
||||
proxy.registerPool(IERC20(_raiToken), IPrivacyPool(address(_raiPool)), 0.1 ether, 1000, 500);
|
||||
|
||||
// Check pool is active
|
||||
assertFalse(_raiPool.dead(), 'Pool must be alive');
|
||||
|
||||
// Fetch stored configuration for RAI pool
|
||||
(_poolAddressFromConfig, _minimumDepositAmountFromConfig, _vettingFeeBPSFromConfig, _maxRelayFeeBPSFromConfig) =
|
||||
proxy.assetConfig(IERC20(_raiToken));
|
||||
|
||||
// Check the configured values match the ones provided by the owner on `registerPool`
|
||||
assertEq(address(_poolAddressFromConfig), address(_raiPool), 'Registered pool address must match');
|
||||
assertEq(_minimumDepositAmountFromConfig, 0.1 ether, 'Minimum deposit amount must match');
|
||||
assertEq(_vettingFeeBPSFromConfig, 1000, 'Vetting fee must match');
|
||||
assertEq(_maxRelayFeeBPSFromConfig, 500, 'Max relay fee must match');
|
||||
assertEq(address(_raiPool), address(proxy.scopeToPool(_poolScope)), 'Registered pool scope must match');
|
||||
|
||||
// As owner, wind down RAI pool
|
||||
vm.prank(owner);
|
||||
proxy.windDownPool(IPrivacyPool(address(_raiPool)));
|
||||
|
||||
// Check pool is disabled
|
||||
assertTrue(_raiPool.dead(), 'Pool must be dead');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the owner can wind down a pool and completely remove its configuration from the Entrypoint
|
||||
*/
|
||||
function test_WindDownAndRemovePool() public {
|
||||
// Check pool is active
|
||||
assertFalse(ethPool.dead(), 'Pool must be alive');
|
||||
|
||||
// Wind down pool
|
||||
vm.prank(owner);
|
||||
proxy.windDownPool(IPrivacyPool(address(ethPool)));
|
||||
|
||||
// Check pool is disabled
|
||||
assertTrue(ethPool.dead(), 'Pool must be dead');
|
||||
|
||||
// Remove pool from configuration
|
||||
vm.prank(owner);
|
||||
proxy.removePool(IERC20(Constants.NATIVE_ASSET));
|
||||
|
||||
// Fetch updated pool configuration
|
||||
(_poolAddressFromConfig, _minimumDepositAmountFromConfig, _vettingFeeBPSFromConfig, _maxRelayFeeBPSFromConfig) =
|
||||
proxy.assetConfig(IERC20(Constants.NATIVE_ASSET));
|
||||
|
||||
// Check all values were zeroe'd
|
||||
assertEq(address(_poolAddressFromConfig), address(0), 'Registered pool address must be address zero');
|
||||
assertEq(_minimumDepositAmountFromConfig, 0, 'Minimum deposit amount must be zero');
|
||||
assertEq(_vettingFeeBPSFromConfig, 0, 'Vetting fee must be zero');
|
||||
assertEq(_maxRelayFeeBPSFromConfig, 0, 'Max relay fee must be zero');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that the owner can withdraw the collected fees from the Entrypoint
|
||||
*/
|
||||
function test_WithdrawFees() public {
|
||||
// Fetch previous balances
|
||||
uint256 _ownerBalanceBefore = owner.balance;
|
||||
uint256 _entrypointBalanceBefore = address(proxy).balance;
|
||||
|
||||
// Withdraw all ETH fees to owner
|
||||
vm.prank(owner);
|
||||
proxy.withdrawFees(IERC20(Constants.NATIVE_ASSET), owner);
|
||||
|
||||
// Check balances were updated correctly
|
||||
assertEq(owner.balance, _ownerBalanceBefore + _entrypointBalanceBefore, 'Owner balance mismatch');
|
||||
assertEq(address(proxy).balance, 0, 'Entrypoint balance should be zero');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that a user can deposit and partially withdraw through a relayer
|
||||
*/
|
||||
function test_DepositAndWithdrawThroughRelayer() public {
|
||||
uint256 _depositAmount = 10 ether;
|
||||
|
||||
// Calculate deposited amount after configured fees
|
||||
_value = _deductFee(_depositAmount, _vettingFeeBPSFromConfig);
|
||||
|
||||
// Deal user
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
// Compute precommitment
|
||||
_precommitment = _hashPrecommitment(_genSecretBySeed('nullifier'), _genSecretBySeed('secret'));
|
||||
|
||||
// Precalculate label
|
||||
uint256 _currentNonce = ethPool.nonce();
|
||||
_label = uint256(keccak256(abi.encodePacked(ethPool.SCOPE(), ++_currentNonce))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Deposit
|
||||
vm.prank(_user);
|
||||
uint256 _commitmentHash = proxy.deposit{value: _depositAmount}(_precommitment);
|
||||
|
||||
// Generate the state merkle proof with the fork state tree and the new leaf (commitment hash)
|
||||
string[] memory _stateMerkleProofInputs = new string[](4);
|
||||
_stateMerkleProofInputs[0] = 'node';
|
||||
_stateMerkleProofInputs[1] = 'test/helper/MerkleProofFromFile.mjs';
|
||||
_stateMerkleProofInputs[2] = 'test/upgrades/leaves_and_roots.csv';
|
||||
_stateMerkleProofInputs[3] = vm.toString(_commitmentHash);
|
||||
bytes memory _stateMerkleProof = vm.ffi(_stateMerkleProofInputs);
|
||||
|
||||
// Create a single-leaf ASP tree with only our label
|
||||
uint256[] memory _leaves = new uint256[](1);
|
||||
_leaves[0] = _label;
|
||||
bytes memory _aspMerkleProof = _generateMerkleProofMemory(_leaves, _label);
|
||||
|
||||
(uint256 _aspRoot,,) = abi.decode(_aspMerkleProof, (uint256, uint256, uint256[]));
|
||||
|
||||
// Push the new root including our label
|
||||
vm.prank(postman);
|
||||
proxy.updateRoot(_aspRoot, 'ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid');
|
||||
|
||||
// Prepare withdrawal for relayer with encoded relay fees data
|
||||
IPrivacyPool.Withdrawal memory _withdrawal =
|
||||
IPrivacyPool.Withdrawal({processooor: address(proxy), data: abi.encode(_recipient, _relayer, 100)});
|
||||
|
||||
// Compute context for proof gen
|
||||
_context = uint256(keccak256(abi.encode(_withdrawal, ethPool.SCOPE()))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Generate Withdrawal proof
|
||||
string[] memory _inputs = new string[](12);
|
||||
_inputs[0] = vm.toString(_value);
|
||||
_inputs[1] = vm.toString(_label);
|
||||
_inputs[2] = vm.toString(_genSecretBySeed('nullifier'));
|
||||
_inputs[3] = vm.toString(_genSecretBySeed('secret'));
|
||||
_inputs[4] = vm.toString(_genSecretBySeed('nullifier_2'));
|
||||
_inputs[5] = vm.toString(_genSecretBySeed('secret_2'));
|
||||
_inputs[6] = vm.toString(uint256(5 ether)); // <--- withdrawn value
|
||||
_inputs[7] = vm.toString(_context);
|
||||
_inputs[8] = vm.toString(_stateMerkleProof);
|
||||
_inputs[9] = vm.toString(uint256(11));
|
||||
_inputs[10] = vm.toString(_aspMerkleProof);
|
||||
_inputs[11] = vm.toString(uint256(11));
|
||||
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
ProofLib.WithdrawProof memory _proof = abi.decode(_proofData, (ProofLib.WithdrawProof));
|
||||
|
||||
// Fetch recipient balance before
|
||||
uint256 _recipientBalanceBefore = _recipient.balance;
|
||||
|
||||
// Relay withdrawal as relayer
|
||||
vm.prank(_relayer);
|
||||
proxy.relay(_withdrawal, _proof, ethPool.SCOPE());
|
||||
|
||||
// Check the balance has correctly changed
|
||||
uint256 _withdrawnAmount = _deductFee(5 ether, _maxRelayFeeBPSFromConfig);
|
||||
assertEq(_recipientBalanceBefore + _withdrawnAmount, _recipient.balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that a user can deposit and partially withdraw directly
|
||||
*/
|
||||
function test_DepositAndWithdrawDirectly() public {
|
||||
uint256 _depositAmount = 10 ether;
|
||||
|
||||
// Calculate deposited amount after configured fees
|
||||
_value = _deductFee(_depositAmount, _vettingFeeBPSFromConfig);
|
||||
|
||||
// Deal user
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
// Compute precommitment
|
||||
_nullifier = _genSecretBySeed('nullifier');
|
||||
_secret = _genSecretBySeed('secret');
|
||||
_precommitment = _hashPrecommitment(_nullifier, _secret);
|
||||
|
||||
// Precalculate label
|
||||
uint256 _currentNonce = ethPool.nonce();
|
||||
_label = uint256(keccak256(abi.encodePacked(ethPool.SCOPE(), ++_currentNonce))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Deposit
|
||||
vm.prank(_user);
|
||||
uint256 _commitmentHash = proxy.deposit{value: _depositAmount}(_precommitment);
|
||||
|
||||
// Generate the state merkle proof with the fork state tree and the new leaf (commitment hash)
|
||||
string[] memory _stateMerkleProofInputs = new string[](4);
|
||||
_stateMerkleProofInputs[0] = 'node';
|
||||
_stateMerkleProofInputs[1] = 'test/helper/MerkleProofFromFile.mjs';
|
||||
_stateMerkleProofInputs[2] = 'test/upgrades/leaves_and_roots.csv';
|
||||
_stateMerkleProofInputs[3] = vm.toString(_commitmentHash);
|
||||
|
||||
bytes memory _stateMerkleProof = vm.ffi(_stateMerkleProofInputs);
|
||||
|
||||
// Create a single-leaf ASP tree with only our label
|
||||
uint256[] memory _leaves = new uint256[](1);
|
||||
_leaves[0] = _label;
|
||||
bytes memory _aspMerkleProof = _generateMerkleProofMemory(_leaves, _label);
|
||||
|
||||
(uint256 _aspRoot,,) = abi.decode(_aspMerkleProof, (uint256, uint256, uint256[]));
|
||||
|
||||
// Push the new root including our label
|
||||
vm.prank(postman);
|
||||
proxy.updateRoot(_aspRoot, 'ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid');
|
||||
|
||||
// Prepare withdrawal without fee data and `recipient` as processooor
|
||||
IPrivacyPool.Withdrawal memory _withdrawal =
|
||||
IPrivacyPool.Withdrawal({processooor: _recipient, data: abi.encode('')});
|
||||
|
||||
// Calculate context for proof gen
|
||||
_context = uint256(keccak256(abi.encode(_withdrawal, ethPool.SCOPE()))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
string[] memory _inputs = new string[](12);
|
||||
_inputs[0] = vm.toString(_value);
|
||||
_inputs[1] = vm.toString(_label);
|
||||
_inputs[2] = vm.toString(_genSecretBySeed('nullifier'));
|
||||
_inputs[3] = vm.toString(_genSecretBySeed('secret'));
|
||||
_inputs[4] = vm.toString(_genSecretBySeed('nullifier_2'));
|
||||
_inputs[5] = vm.toString(_genSecretBySeed('secret_2'));
|
||||
_inputs[6] = vm.toString(uint256(5 ether)); // <--- withdrawn value
|
||||
_inputs[7] = vm.toString(_context);
|
||||
_inputs[8] = vm.toString(_stateMerkleProof);
|
||||
_inputs[9] = vm.toString(uint256(11));
|
||||
_inputs[10] = vm.toString(_aspMerkleProof);
|
||||
_inputs[11] = vm.toString(uint256(11));
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
ProofLib.WithdrawProof memory _proof = abi.decode(_proofData, (ProofLib.WithdrawProof));
|
||||
|
||||
uint256 _recipientBalanceBefore = _recipient.balance;
|
||||
|
||||
vm.prank(_recipient);
|
||||
ethPool.withdraw(_withdrawal, _proof);
|
||||
|
||||
assertEq(_recipientBalanceBefore + 5 ether, _recipient.balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that a user can deposit, partially withdraw and ragequit
|
||||
*/
|
||||
function test_DepositWithdrawAndRagequit() public {
|
||||
uint256 _depositAmount = 5 ether;
|
||||
|
||||
// Calculate deposited amount after configured fees
|
||||
_value = _deductFee(_depositAmount, _vettingFeeBPSFromConfig);
|
||||
|
||||
// Deal user
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
// Compute precommitment
|
||||
_nullifier = _genSecretBySeed('nullifier');
|
||||
_secret = _genSecretBySeed('secret');
|
||||
_precommitment = _hashPrecommitment(_nullifier, _secret);
|
||||
|
||||
// Precalculate label
|
||||
uint256 _currentNonce = ethPool.nonce();
|
||||
_label = uint256(keccak256(abi.encodePacked(ethPool.SCOPE(), ++_currentNonce))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Deposit
|
||||
vm.prank(_user);
|
||||
uint256 _commitmentHash = proxy.deposit{value: _depositAmount}(_precommitment);
|
||||
|
||||
// Generate the state merkle proof with the fork state tree and the new leaf (commitment hash)
|
||||
string[] memory _stateMerkleProofInputs = new string[](4);
|
||||
_stateMerkleProofInputs[0] = 'node';
|
||||
_stateMerkleProofInputs[1] = 'test/helper/MerkleProofFromFile.mjs';
|
||||
_stateMerkleProofInputs[2] = 'test/upgrades/leaves_and_roots.csv';
|
||||
_stateMerkleProofInputs[3] = vm.toString(_commitmentHash);
|
||||
|
||||
bytes memory _stateMerkleProof = vm.ffi(_stateMerkleProofInputs);
|
||||
|
||||
// Create a single-leaf ASP tree with only our label
|
||||
uint256[] memory _leaves = new uint256[](1);
|
||||
_leaves[0] = _label;
|
||||
bytes memory _aspMerkleProof = _generateMerkleProofMemory(_leaves, _label);
|
||||
(uint256 _aspRoot,,) = abi.decode(_aspMerkleProof, (uint256, uint256, uint256[]));
|
||||
|
||||
// Push new root with our label
|
||||
vm.prank(postman);
|
||||
proxy.updateRoot(_aspRoot, 'ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid');
|
||||
|
||||
// Prepare direct withdrawal
|
||||
IPrivacyPool.Withdrawal memory _withdrawal =
|
||||
IPrivacyPool.Withdrawal({processooor: _recipient, data: abi.encode('')});
|
||||
|
||||
// Compute context for proof gen
|
||||
_context = uint256(keccak256(abi.encode(_withdrawal, ethPool.SCOPE()))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Generate Withdrawal proof
|
||||
string[] memory _inputs = new string[](12);
|
||||
_inputs[0] = vm.toString(_value);
|
||||
_inputs[1] = vm.toString(_label);
|
||||
_inputs[2] = vm.toString(_genSecretBySeed('nullifier'));
|
||||
_inputs[3] = vm.toString(_genSecretBySeed('secret'));
|
||||
_inputs[4] = vm.toString(_genSecretBySeed('nullifier_2'));
|
||||
_inputs[5] = vm.toString(_genSecretBySeed('secret_2'));
|
||||
_inputs[6] = vm.toString(uint256(2 ether)); // <--- withdrawn value
|
||||
_inputs[7] = vm.toString(_context);
|
||||
_inputs[8] = vm.toString(_stateMerkleProof);
|
||||
_inputs[9] = vm.toString(uint256(11));
|
||||
_inputs[10] = vm.toString(_aspMerkleProof);
|
||||
_inputs[11] = vm.toString(uint256(11));
|
||||
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
_withdrawProof = abi.decode(_proofData, (ProofLib.WithdrawProof));
|
||||
|
||||
// Fetch recipient balance before withdrawal
|
||||
uint256 _recipientBalanceBefore = _recipient.balance;
|
||||
|
||||
// Successfully withdraw
|
||||
vm.prank(_recipient);
|
||||
ethPool.withdraw(_withdrawal, _withdrawProof);
|
||||
|
||||
// Check balance was correctly updated
|
||||
assertEq(_recipientBalanceBefore + 2 ether, _recipient.balance, 'Recipient balance mismatch');
|
||||
_value -= 2 ether;
|
||||
|
||||
// Generate ragequit proof
|
||||
_ragequitProof =
|
||||
_generateRagequitProof(_value, _label, _genSecretBySeed('nullifier_2'), _genSecretBySeed('secret_2'));
|
||||
|
||||
// Fetch user balance before ragequitting
|
||||
uint256 _userBalanceBefore = _user.balance;
|
||||
|
||||
// Call `ragequit` as original depositor
|
||||
vm.prank(_user);
|
||||
ethPool.ragequit(_ragequitProof);
|
||||
|
||||
// Check balance was correctly updated
|
||||
assertEq(_userBalanceBefore + _value, _user.balance, 'User balance mismatch');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Test that a user can deposit and completely ragequit
|
||||
*/
|
||||
function test_DepositAndRagequit() public {
|
||||
uint256 _depositAmount = 2 ether;
|
||||
|
||||
// Calculate deposited amount after configured fees
|
||||
_value = _deductFee(_depositAmount, _vettingFeeBPSFromConfig);
|
||||
|
||||
// Deal user
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
// Compute precommitment
|
||||
_nullifier = _genSecretBySeed('nullifier');
|
||||
_secret = _genSecretBySeed('secret');
|
||||
_precommitment = _hashPrecommitment(_nullifier, _secret);
|
||||
|
||||
// Precompute label
|
||||
uint256 _currentNonce = ethPool.nonce();
|
||||
_label = uint256(keccak256(abi.encodePacked(ethPool.SCOPE(), ++_currentNonce))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Deposit
|
||||
vm.prank(_user);
|
||||
proxy.deposit{value: _depositAmount}(_precommitment);
|
||||
|
||||
// Don't approve anything ASP-wise
|
||||
|
||||
// Generate ragequit proof
|
||||
ProofLib.RagequitProof memory _proof = _generateRagequitProof(_value, _label, _nullifier, _secret);
|
||||
|
||||
// Fetch user balance before ragequitting
|
||||
uint256 _userBalanceBefore = _user.balance;
|
||||
|
||||
// Ragequit full commitment as the original depositor
|
||||
vm.prank(_user);
|
||||
ethPool.ragequit(_proof);
|
||||
|
||||
// Check the balance was correctly updated
|
||||
assertEq(_userBalanceBefore + _value, _user.balance, 'User balance mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Testing a deposit+withdrawal with the upgrade in between
|
||||
*/
|
||||
contract EntrypointBeforeAndAfterIntegration is Test, IntegrationUtils, MainnetEnvironment {
|
||||
uint256 internal _value;
|
||||
uint256 internal _label;
|
||||
uint256 internal _precommitment;
|
||||
uint256 internal _nullifier;
|
||||
uint256 internal _secret;
|
||||
uint256 internal _context;
|
||||
uint256 internal _vettingFeeBPS;
|
||||
|
||||
address internal _user = makeAddr('user');
|
||||
address internal _recipient = makeAddr('recipient');
|
||||
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
|
||||
|
||||
function setUp() public {
|
||||
// Fork mainnet without upgrading
|
||||
vm.createSelectFork(vm.rpcUrl('mainnet'), _FORK_BLOCK);
|
||||
|
||||
// Fetch vetting fee for value calculation
|
||||
(,, _vettingFeeBPS,) = proxy.assetConfig(IERC20(Constants.NATIVE_ASSET));
|
||||
}
|
||||
|
||||
function test_DepositAndWithdrawMidUpgrade() public {
|
||||
uint256 _depositAmount = 2 ether;
|
||||
|
||||
// Calculate deposited amount after configured fees
|
||||
_value = _deductFee(_depositAmount, _vettingFeeBPS);
|
||||
|
||||
// Deal user
|
||||
vm.deal(_user, _depositAmount);
|
||||
|
||||
// Compute precommitment
|
||||
_nullifier = _genSecretBySeed('nullifier');
|
||||
_secret = _genSecretBySeed('secret');
|
||||
_precommitment = _hashPrecommitment(_nullifier, _secret);
|
||||
|
||||
// Precalculate label
|
||||
uint256 _currentNonce = ethPool.nonce();
|
||||
_label = uint256(keccak256(abi.encodePacked(ethPool.SCOPE(), ++_currentNonce))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
// Deposit
|
||||
vm.prank(_user);
|
||||
uint256 _commitmentHash = proxy.deposit{value: _depositAmount}(_precommitment);
|
||||
|
||||
//////////////////////////////////////// CONTRACT UPRGADE : START ////////////////////////////////////////
|
||||
|
||||
// Deploy new implementation
|
||||
Entrypoint _newImplementation = new Entrypoint();
|
||||
|
||||
// Upgrade Entrypoint
|
||||
vm.prank(owner);
|
||||
proxy.upgradeToAndCall(address(_newImplementation), '');
|
||||
|
||||
// Check the implementation was successfully updated in the proxy storage
|
||||
bytes32 _implementationAddressRaw = vm.load(address(proxy), _IMPLEMENTATION_SLOT);
|
||||
assertEq(
|
||||
address(uint160(uint256(_implementationAddressRaw))),
|
||||
address(_newImplementation),
|
||||
"Implementation addresses don't match"
|
||||
);
|
||||
|
||||
////////////////////////////////////// CONTRACT UPRGADE : END ////////////////////////////////////////
|
||||
|
||||
// Generate the state merkle proof with the fork state tree and the new leaf (commitment hash)
|
||||
string[] memory _stateMerkleProofInputs = new string[](4);
|
||||
_stateMerkleProofInputs[0] = 'node';
|
||||
_stateMerkleProofInputs[1] = 'test/helper/MerkleProofFromFile.mjs';
|
||||
_stateMerkleProofInputs[2] = 'test/upgrades/leaves_and_roots.csv';
|
||||
_stateMerkleProofInputs[3] = vm.toString(_commitmentHash);
|
||||
|
||||
bytes memory _stateMerkleProof = vm.ffi(_stateMerkleProofInputs);
|
||||
|
||||
// Create a single-leaf ASP tree with only our label
|
||||
uint256[] memory _leaves = new uint256[](1);
|
||||
_leaves[0] = _label;
|
||||
bytes memory _aspMerkleProof = _generateMerkleProofMemory(_leaves, _label);
|
||||
|
||||
(uint256 _aspRoot,,) = abi.decode(_aspMerkleProof, (uint256, uint256, uint256[]));
|
||||
|
||||
// Push the new root including our label
|
||||
vm.prank(postman);
|
||||
proxy.updateRoot(_aspRoot, 'ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid_ipfs_cid');
|
||||
|
||||
// Prepare withdrawal without fee data and `recipient` as processooor
|
||||
IPrivacyPool.Withdrawal memory _withdrawal =
|
||||
IPrivacyPool.Withdrawal({processooor: _recipient, data: abi.encode('')});
|
||||
|
||||
// Calculate context for proof gen
|
||||
_context = uint256(keccak256(abi.encode(_withdrawal, ethPool.SCOPE()))) % Constants.SNARK_SCALAR_FIELD;
|
||||
|
||||
string[] memory _inputs = new string[](12);
|
||||
_inputs[0] = vm.toString(_value);
|
||||
_inputs[1] = vm.toString(_label);
|
||||
_inputs[2] = vm.toString(_genSecretBySeed('nullifier'));
|
||||
_inputs[3] = vm.toString(_genSecretBySeed('secret'));
|
||||
_inputs[4] = vm.toString(_genSecretBySeed('nullifier_2'));
|
||||
_inputs[5] = vm.toString(_genSecretBySeed('secret_2'));
|
||||
_inputs[6] = vm.toString(uint256(1 ether)); // <--- withdrawn value
|
||||
_inputs[7] = vm.toString(_context);
|
||||
_inputs[8] = vm.toString(_stateMerkleProof);
|
||||
_inputs[9] = vm.toString(uint256(11));
|
||||
_inputs[10] = vm.toString(_aspMerkleProof);
|
||||
_inputs[11] = vm.toString(uint256(11));
|
||||
|
||||
// Call the ProofGenerator script using node
|
||||
string[] memory _scriptArgs = new string[](2);
|
||||
_scriptArgs[0] = 'node';
|
||||
_scriptArgs[1] = 'test/helper/WithdrawalProofGenerator.mjs';
|
||||
bytes memory _proofData = vm.ffi(_concat(_scriptArgs, _inputs));
|
||||
|
||||
ProofLib.WithdrawProof memory _proof = abi.decode(_proofData, (ProofLib.WithdrawProof));
|
||||
|
||||
uint256 _recipientBalanceBefore = _recipient.balance;
|
||||
|
||||
// Successfully withdraw after upgrade
|
||||
vm.prank(_recipient);
|
||||
ethPool.withdraw(_withdrawal, _proof);
|
||||
|
||||
assertEq(_recipientBalanceBefore + 1 ether, _recipient.balance);
|
||||
}
|
||||
}
|
||||
1284
packages/contracts/test/upgrades/leaves_and_roots.csv
Normal file
1284
packages/contracts/test/upgrades/leaves_and_roots.csv
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user