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:
moebius
2025-05-21 09:35:28 -06:00
committed by GitHub
parent 918978d542
commit 7ccea5c4f2
13 changed files with 2823 additions and 228 deletions

View File

@@ -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`

View File

@@ -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();

View File

@@ -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);
}

View 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);

View 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));

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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) {

View File

@@ -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);
}
}
/**

View File

@@ -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

View 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);
}
}

File diff suppressed because it is too large Load Diff