mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-09 01:17:58 -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:
@@ -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