fix: internal review (#80)

This pull requests introduces the fixes to the internal review findings.
Tests for covering the new error cases have been added.
This commit is contained in:
moebius
2025-02-14 10:20:59 +01:00
committed by GitHub
parent 6d725b16de
commit 13bbc96dc7
23 changed files with 707 additions and 320 deletions

View File

@@ -30,7 +30,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable
- name: Use Node.js
uses: actions/setup-node@v4

View File

@@ -21,7 +21,7 @@ template Withdraw(maxTreeDepth) {
signal input stateTreeDepth; // Current state tree depth
signal input ASPRoot; // Latest ASP root
signal input ASPTreeDepth; // Current ASP tree depth
signal input context; // keccak256(scope, Withdrawal)
signal input context; // keccak256(IPrivacyPool.Withdrawal, scope)
//////////////////// END OF PUBLIC SIGNALS ////////////////////

View File

@@ -23,3 +23,6 @@ crytic-export
# Echidna corpus
test/invariants/fuzz/echidna_coverage
# Coverage files
lcov.info

View File

@@ -31,11 +31,11 @@
},
"dependencies": {
"@defi-wonderland/privacy-pool-core-sdk": "0.1.0",
"@openzeppelin/contracts": "^5.1.0",
"@openzeppelin/contracts": "5.1.0",
"@openzeppelin/contracts-upgradeable": "5.0.2",
"@openzeppelin/foundry-upgrades": "^0.3.6",
"@zk-kit/lean-imt": "^2.2.2",
"@zk-kit/lean-imt.sol": "^2.0.0",
"@openzeppelin/foundry-upgrades": "0.3.6",
"@zk-kit/lean-imt": "2.2.2",
"@zk-kit/lean-imt.sol": "2.0.0",
"poseidon-solidity": "^0.0.5",
"solc": "0.8.28"
},
@@ -44,7 +44,7 @@
"@commitlint/config-conventional": "19.2.2",
"@defi-wonderland/natspec-smells": "1.1.3",
"@types/node": "^22.10.10",
"forge-std": "github:foundry-rs/forge-std#1.9.2",
"forge-std": "github:foundry-rs/forge-std#1.9.6",
"halmos-cheatcodes": "github:a16z/halmos-cheatcodes#c0d8655",
"husky": ">=9",
"lint-staged": ">=10",

View File

@@ -22,7 +22,6 @@ import {ReentrancyGuardUpgradeable} from '@oz-upgradeable/utils/ReentrancyGuardU
import {SafeERC20} from '@oz/token/ERC20/utils/SafeERC20.sol';
import {IERC20} from '@oz/interfaces/IERC20.sol';
import {SafeERC20} from '@oz/token/ERC20/utils/SafeERC20.sol';
import {Constants} from './lib/Constants.sol';
import {ProofLib} from './lib/ProofLib.sol';
@@ -69,12 +68,15 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
if (_owner == address(0)) revert ZeroAddress();
if (_postman == address(0)) revert ZeroAddress();
// Initialize upgradeable contractcs
// Initialize upgradeable contracts
__UUPSUpgradeable_init();
__ReentrancyGuard_init();
__AccessControl_init();
// Initialize roles
_setRoleAdmin(DEFAULT_ADMIN_ROLE, _OWNER_ROLE);
_setRoleAdmin(_OWNER_ROLE, _OWNER_ROLE); // Owner can manage owner role
_setRoleAdmin(_ASP_POSTMAN, _OWNER_ROLE); // Owner can manage postman role
_grantRole(_OWNER_ROLE, _owner);
_grantRole(_ASP_POSTMAN, _postman);
@@ -102,13 +104,17 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
//////////////////////////////////////////////////////////////*/
/// @inheritdoc IEntrypoint
function deposit(uint256 _precommitment) external payable returns (uint256 _commitment) {
function deposit(uint256 _precommitment) external payable nonReentrant returns (uint256 _commitment) {
// Handle deposit as native asset
_commitment = _handleDeposit(IERC20(Constants.NATIVE_ASSET), msg.value, _precommitment);
}
/// @inheritdoc IEntrypoint
function deposit(IERC20 _asset, uint256 _value, uint256 _precommitment) external returns (uint256 _commitment) {
function deposit(
IERC20 _asset,
uint256 _value,
uint256 _precommitment
) external nonReentrant returns (uint256 _commitment) {
// Pull funds from user
_asset.safeTransferFrom(msg.sender, address(this), _value);
// Handle deposit as ERC20
@@ -122,7 +128,8 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
/// @inheritdoc IEntrypoint
function relay(
IPrivacyPool.Withdrawal calldata _withdrawal,
ProofLib.WithdrawProof calldata _proof
ProofLib.WithdrawProof calldata _proof,
uint256 _scope
) external nonReentrant {
// Check withdrawn amount is non-zero
if (_proof.withdrawnValue() == 0) revert InvalidWithdrawalAmount();
@@ -130,7 +137,7 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
if (_withdrawal.processooor != address(this)) revert InvalidProcessooor();
// Fetch pool by scope
IPrivacyPool _pool = scopeToPool[_withdrawal.scope];
IPrivacyPool _pool = scopeToPool[_scope];
if (address(_pool) == address(0)) revert PoolNotFound();
// Store pool asset
@@ -140,8 +147,8 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
// Process withdrawal
_pool.withdraw(_withdrawal, _proof);
// Decode fee data
FeeData memory _data = abi.decode(_withdrawal.data, (FeeData));
// Decode relay data
RelayData memory _data = abi.decode(_withdrawal.data, (RelayData));
uint256 _withdrawnAmount = _proof.withdrawnValue();
// Deduct fees
@@ -170,25 +177,25 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
uint256 _minimumDepositAmount,
uint256 _vettingFeeBPS
) external onlyRole(_OWNER_ROLE) {
// Sanity check values
// Sanity check addresses
if (address(_asset) == address(0)) revert ZeroAddress();
if (address(_pool) == address(0)) revert ZeroAddress();
// Vetting fee can't be greater than 100%
if (_vettingFeeBPS > 10_000) revert InvalidFeeBPS();
// Fetch pool configuration
AssetConfig storage _config = assetConfig[_asset];
if (address(_config.pool) != address(0)) revert AssetPoolAlreadyRegistered();
// Fetch pool scope
// Fetch pool scope and validate asset
uint256 _scope = _pool.SCOPE();
if (address(scopeToPool[_scope]) != address(0)) revert ScopePoolAlreadyRegistered();
if (_asset != IERC20(_pool.ASSET())) revert AssetMismatch();
// Store pool configuration
scopeToPool[_scope] = _pool;
_config.pool = _pool;
_config.minimumDepositAmount = _minimumDepositAmount;
_config.vettingFeeBPS = _vettingFeeBPS;
// Update pool configuration with validation
_setPoolConfiguration(_config, _minimumDepositAmount, _vettingFeeBPS);
// If asset is an ERC20, approve pool to spend
if (address(_asset) != Constants.NATIVE_ASSET) _asset.approve(address(_pool), type(uint256).max);
@@ -221,16 +228,12 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
uint256 _minimumDepositAmount,
uint256 _vettingFeeBPS
) external onlyRole(_OWNER_ROLE) {
// Check fee is less than 100%
if (_vettingFeeBPS > 10_000) revert InvalidFeeBPS();
// Fetch pool configuration
AssetConfig storage _config = assetConfig[_asset];
if (address(_config.pool) == address(0)) revert PoolNotFound();
// Update asset configuration
_config.minimumDepositAmount = _minimumDepositAmount;
_config.vettingFeeBPS = _vettingFeeBPS;
// Update pool configuration with validation
_setPoolConfiguration(_config, _minimumDepositAmount, _vettingFeeBPS);
emit PoolConfigurationUpdated(_config.pool, _asset, _minimumDepositAmount, _vettingFeeBPS);
}
@@ -274,7 +277,14 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
RECEIVE
//////////////////////////////////////////////////////////////*/
receive() external payable {}
/**
* @notice Needed to receive native asset from a pool when withdrawing
* @dev Only accepts native asset from the local native asset pool
*/
receive() external payable {
address _nativePool = address(assetConfig[IERC20(Constants.NATIVE_ASSET)].pool);
if (msg.sender != _nativePool) revert NativeAssetNotAccepted();
}
/*///////////////////////////////////////////////////////////////
INTERNAL METHODS
@@ -348,4 +358,23 @@ contract Entrypoint is AccessControlUpgradeable, UUPSUpgradeable, ReentrancyGuar
function _deductFee(uint256 _amount, uint256 _feeBPS) internal pure returns (uint256 _afterFees) {
_afterFees = _amount - (_amount * _feeBPS / 10_000);
}
/**
* @notice Sets pool configuration parameters with validation
* @dev Validates and sets minimum deposit amount and vetting fee
* @param _config The pool configuration to update
* @param _minimumDepositAmount The new minimum deposit amount
* @param _vettingFeeBPS The new vetting fee in basis points
*/
function _setPoolConfiguration(
AssetConfig storage _config,
uint256 _minimumDepositAmount,
uint256 _vettingFeeBPS
) internal {
// Check fee is less than 100%
if (_vettingFeeBPS >= 10_000) revert InvalidFeeBPS();
_config.minimumDepositAmount = _minimumDepositAmount;
_config.vettingFeeBPS = _vettingFeeBPS;
}
}

View File

@@ -35,24 +35,21 @@ abstract contract PrivacyPool is State, IPrivacyPool {
using ProofLib for ProofLib.WithdrawProof;
using ProofLib for ProofLib.RagequitProof;
/// @inheritdoc IPrivacyPool
uint256 public immutable SCOPE;
/// @inheritdoc IPrivacyPool
address public immutable ASSET;
/**
* @notice Does a series of sanity checks on the proof public signals
*/
modifier validWithdrawal(Withdrawal memory _withdrawal, ProofLib.WithdrawProof memory _proof) {
// Check caller is the allowed processooor
if (msg.sender != _withdrawal.processooor) revert InvalidProcesooor();
if (msg.sender != _withdrawal.processooor) revert InvalidProcessooor();
// Check the context matches to ensure its integrity
if (_proof.context() != uint256(keccak256(abi.encode(_withdrawal, SCOPE))) % Constants.SNARK_SCALAR_FIELD) {
revert ContextMismatch();
}
// Check the tree depth signals are less than the max tree depth
if (_proof.stateTreeDepth() > MAX_TREE_DEPTH || _proof.ASPTreeDepth() > MAX_TREE_DEPTH) revert InvalidTreeDepth();
// Check the state root is known
if (!_isKnownRoot(_proof.stateRoot())) revert UnknownStateRoot();
@@ -73,18 +70,7 @@ abstract contract PrivacyPool is State, IPrivacyPool {
address _withdrawalVerifier,
address _ragequitVerifier,
address _asset
) State(_entrypoint, _withdrawalVerifier, _ragequitVerifier) {
// Sanitize initial addresses
if (_asset == address(0)) revert ZeroAddress();
if (_entrypoint == address(0)) revert ZeroAddress();
if (_ragequitVerifier == address(0)) revert ZeroAddress();
if (_withdrawalVerifier == address(0)) revert ZeroAddress();
// Store asset address
ASSET = _asset;
// Compute SCOPE
SCOPE = uint256(keccak256(abi.encodePacked(address(this), block.chainid, _asset))) % Constants.SNARK_SCALAR_FIELD;
}
) State(_asset, _entrypoint, _withdrawalVerifier, _ragequitVerifier) {}
/*///////////////////////////////////////////////////////////////
USER METHODS
@@ -101,8 +87,8 @@ abstract contract PrivacyPool is State, IPrivacyPool {
// Compute label
uint256 _label = uint256(keccak256(abi.encodePacked(SCOPE, ++nonce))) % Constants.SNARK_SCALAR_FIELD;
// Store depositor and ragequit cooldown
deposits[_label] = Deposit(_depositor, _value, block.timestamp + 1 weeks);
// Store depositor
depositors[_label] = _depositor;
// Compute commitment hash
_commitment = PoseidonT4.hash([_value, _label, _precommitmentHash]);
@@ -142,7 +128,7 @@ abstract contract PrivacyPool is State, IPrivacyPool {
function ragequit(ProofLib.RagequitProof memory _proof) external {
// Check if caller is original depositor
uint256 _label = _proof.label();
if (deposits[_label].depositor != msg.sender) revert OnlyOriginalDepositor();
if (depositors[_label] != msg.sender) revert OnlyOriginalDepositor();
// Verify proof with Groth16 verifier
if (!RAGEQUIT_VERIFIER.verifyProof(_proof.pA, _proof.pB, _proof.pC, _proof.pubSignals)) revert InvalidProof();
@@ -150,7 +136,7 @@ abstract contract PrivacyPool is State, IPrivacyPool {
// Check commitment exists in state
if (!_isInState(_proof.commitmentHash())) revert InvalidCommitment();
// Mark nullifier hash as pending for ragequit
// Mark existing commitment nullifier as spent
_spend(_proof.nullifierHash());
// Transfer out funds to ragequitter

View File

@@ -26,15 +26,20 @@ import {IVerifier} from 'interfaces/IVerifier.sol';
/**
* @title State
* @notice Base contract for the managing the state of a Privacy Pool
* @custom:semver 0.1.0
*/
abstract contract State is IState {
using InternalLeanIMT for LeanIMTData;
/// @inheritdoc IState
string public constant VERSION = '0.1.0';
/// @inheritdoc IState
uint32 public constant ROOT_HISTORY_SIZE = 30;
/// @inheritdoc IState
uint32 public constant MAX_TREE_DEPTH = 32;
/// @inheritdoc IState
address public immutable ASSET;
/// @inheritdoc IState
uint256 public immutable SCOPE;
/// @inheritdoc IState
IEntrypoint public immutable ENTRYPOINT;
/// @inheritdoc IState
@@ -58,7 +63,7 @@ abstract contract State is IState {
/// @inheritdoc IState
mapping(uint256 _nullifierHash => bool _spent) public nullifierHashes;
/// @inheritdoc IState
mapping(uint256 _label => Deposit _deposit) public deposits;
mapping(uint256 _label => address _depositooor) public depositors;
/**
* @notice Check the caller is the Entrypoint
@@ -71,7 +76,18 @@ abstract contract State is IState {
/**
* @notice Initialize the state addresses
*/
constructor(address _entrypoint, address _withdrawalVerifier, address _ragequitVerifier) {
constructor(address _asset, address _entrypoint, address _withdrawalVerifier, address _ragequitVerifier) {
// Sanitize initial addresses
if (_asset == address(0)) revert ZeroAddress();
if (_entrypoint == address(0)) revert ZeroAddress();
if (_ragequitVerifier == address(0)) revert ZeroAddress();
if (_withdrawalVerifier == address(0)) revert ZeroAddress();
// Store asset address
ASSET = _asset;
// Compute SCOPE
SCOPE = uint256(keccak256(abi.encodePacked(address(this), block.chainid, _asset))) % Constants.SNARK_SCALAR_FIELD;
ENTRYPOINT = IEntrypoint(_entrypoint);
WITHDRAWAL_VERIFIER = IVerifier(_withdrawalVerifier);
RAGEQUIT_VERIFIER = IVerifier(_ragequitVerifier);
@@ -115,12 +131,15 @@ abstract contract State is IState {
/**
* @notice Inserts a leaf into the state
* @dev Reverts if the leaf is already in the state. Deletes the oldest known root
* @dev A circular buffer is used for root storage to decrease the cost of storing new roots
* @param _leaf The leaf to insert
*/
function _insert(uint256 _leaf) internal returns (uint256 _updatedRoot) {
// Insert leaf in the tree
_updatedRoot = _merkleTree._insert(_leaf);
if (_merkleTree.depth > MAX_TREE_DEPTH) revert MaxTreeDepthReached();
// Calculate the new root index (circular buffer)
uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
@@ -128,21 +147,27 @@ abstract contract State is IState {
currentRootIndex = newRootIndex;
// Store the new root at the new index
roots[newRootIndex] = _updatedRoot % Constants.SNARK_SCALAR_FIELD;
roots[newRootIndex] = _updatedRoot;
emit LeafInserted(_merkleTree.size, _leaf, _updatedRoot);
}
/**
* @notice Returns whether the root is a known root
* @dev A circular buffer is used for root storage to decrease the cost of storing new roots
* @param _root The root to check
*/
function _isKnownRoot(uint256 _root) internal view returns (bool) {
if (_root == 0) return false;
// Iterate the root circular buffer to find the root
for (uint32 _i = 1; _i <= ROOT_HISTORY_SIZE; ++_i) {
if (roots[_i] == _root) return true;
uint32 _currentRootIndex = currentRootIndex;
uint32 _index = _currentRootIndex;
// Check ROOT_HISTORY_SIZE indices, starting from current
for (uint32 _i = 0; _i < ROOT_HISTORY_SIZE; _i++) {
if (_root == roots[_index]) return true;
// Move to previous index, wrap to ROOT_HISTORY_SIZE-1 if we go below 0
_index = _index > 0 ? _index - 1 : ROOT_HISTORY_SIZE - 1;
}
return false;

View File

@@ -18,9 +18,11 @@ https://defi.sucks/
import {IERC20, SafeERC20} from '@oz/token/ERC20/utils/SafeERC20.sol';
import {Constants} from 'contracts/lib/Constants.sol';
import {IPrivacyPoolComplex} from 'interfaces/IPrivacyPool.sol';
import {PrivacyPool} from '../PrivacyPool.sol';
import {PrivacyPool} from 'contracts/PrivacyPool.sol';
/**
* @title PrivacyPoolComplex
@@ -35,7 +37,9 @@ contract PrivacyPoolComplex is PrivacyPool, IPrivacyPoolComplex {
address _withdrawalVerifier,
address _ragequitVerifier,
address _asset
) PrivacyPool(_entrypoint, _withdrawalVerifier, _ragequitVerifier, _asset) {}
) PrivacyPool(_entrypoint, _withdrawalVerifier, _ragequitVerifier, _asset) {
if (_asset == Constants.NATIVE_ASSET) revert NativeAssetNotSupported();
}
/**
* @notice Handle pulling an ERC20 asset

View File

@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
/**
* @title ProofLib
* @notice Facilitates accessing the public signals of a Groth16 proof.
* @custom:semver 0.1.0
*/
library ProofLib {
/*///////////////////////////////////////////////////////////////
@@ -33,17 +34,12 @@ library ProofLib {
uint256[8] pubSignals;
}
/**
* @notice Semantic version of the library
*/
string public constant VERSION = '0.1.0';
/**
* @notice Retrieves the new commitment hash from the proof's public signals
* @param _p The proof containing the public signals
* @return The hash of the new commitment being created
*/
function newCommitmentHash(WithdrawProof memory _p) public pure returns (uint256) {
function newCommitmentHash(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[0];
}
@@ -52,7 +48,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The hash of the nullifier being spent in this withdrawal
*/
function existingNullifierHash(WithdrawProof memory _p) public pure returns (uint256) {
function existingNullifierHash(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[1];
}
@@ -61,7 +57,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The amount being withdrawn from Privacy Pool
*/
function withdrawnValue(WithdrawProof memory _p) public pure returns (uint256) {
function withdrawnValue(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[2];
}
@@ -70,7 +66,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The root of the state tree at time of proof generation
*/
function stateRoot(WithdrawProof memory _p) public pure returns (uint256) {
function stateRoot(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[3];
}
@@ -79,7 +75,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The depth of the state tree at time of proof generation
*/
function stateTreeDepth(WithdrawProof memory _p) public pure returns (uint256) {
function stateTreeDepth(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[4];
}
@@ -88,7 +84,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The latest root of the ASP tree at time of proof generation
*/
function ASPRoot(WithdrawProof memory _p) public pure returns (uint256) {
function ASPRoot(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[5];
}
@@ -97,7 +93,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The depth of the ASP tree at time of proof generation
*/
function ASPTreeDepth(WithdrawProof memory _p) public pure returns (uint256) {
function ASPTreeDepth(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[6];
}
@@ -106,7 +102,7 @@ library ProofLib {
* @param _p The proof containing the public signals
* @return The context value binding the proof to specific withdrawal data
*/
function context(WithdrawProof memory _p) public pure returns (uint256) {
function context(WithdrawProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[7];
}
@@ -139,7 +135,7 @@ library ProofLib {
* @param _p The ragequit proof containing the public signals
* @return The new commitment hash
*/
function commitmentHash(RagequitProof memory _p) public pure returns (uint256) {
function commitmentHash(RagequitProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[0];
}
@@ -148,7 +144,7 @@ library ProofLib {
* @param _p The ragequit proof containing the public signals
* @return The precommitment hash
*/
function precommitmentHash(RagequitProof memory _p) public pure returns (uint256) {
function precommitmentHash(RagequitProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[1];
}
@@ -157,7 +153,7 @@ library ProofLib {
* @param _p The ragequit proof containing the public signals
* @return The nullifier hash
*/
function nullifierHash(RagequitProof memory _p) public pure returns (uint256) {
function nullifierHash(RagequitProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[2];
}
@@ -166,7 +162,7 @@ library ProofLib {
* @param _p The ragequit proof containing the public signals
* @return The commitment value
*/
function value(RagequitProof memory _p) public pure returns (uint256) {
function value(RagequitProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[3];
}
@@ -175,7 +171,7 @@ library ProofLib {
* @param _p The ragequit proof containing the public signals
* @return The commitment label
*/
function label(RagequitProof memory _p) public pure returns (uint256) {
function label(RagequitProof memory _p) internal pure returns (uint256) {
return _p.pubSignals[4];
}
}

View File

@@ -28,12 +28,12 @@ interface IEntrypoint {
}
/**
* @notice Struct for the relay fee data
* @notice Struct for the relay data
* @param recipient The recipient of the funds withdrawn from the pool
* @param feeRecipient The recipient of the fee
* @param relayfeeBPS The relay fee in basis points
*/
struct FeeData {
struct RelayData {
address recipient;
address feeRecipient;
uint256 relayFeeBPS;
@@ -199,6 +199,16 @@ interface IEntrypoint {
*/
error NoRootsAvailable();
/**
* @notice Thrown when trying to register a pool with an asset that doesn't match the pool's asset
*/
error AssetMismatch();
/**
* @notice Thrown when trying to send native asset to the Entrypoint
*/
error NativeAssetNotAccepted();
/*//////////////////////////////////////////////////////////////
LOGIC
//////////////////////////////////////////////////////////////*/
@@ -237,8 +247,13 @@ interface IEntrypoint {
* @notice Process a withdrawal
* @param _withdrawal The `Withdrawal` struct
* @param _proof The `WithdrawProof` struct containing the withdarawal proof signals
* @param _scope The Pool scope to withdraw from
*/
function relay(IPrivacyPool.Withdrawal calldata _withdrawal, ProofLib.WithdrawProof calldata _proof) external;
function relay(
IPrivacyPool.Withdrawal calldata _withdrawal,
ProofLib.WithdrawProof calldata _proof,
uint256 _scope
) external;
/**
* @notice Register a Privacy Pool in the registry

View File

@@ -17,12 +17,10 @@ interface IPrivacyPool is IState {
* @notice Struct for the withdrawal request
* @dev The integrity of this data is ensured by the `context` signal in the proof
* @param processooor The allowed address to process the withdrawal
* @param scope The unique pool identifier
* @param data Encoded arbitrary data used by the Entrypoint
*/
struct Withdrawal {
address processooor;
uint256 scope;
bytes data;
}
@@ -80,7 +78,12 @@ interface IPrivacyPool is IState {
/**
* @notice Thrown when calling `withdraw` while not being the allowed processooor
*/
error InvalidProcesooor();
error InvalidProcessooor();
/**
* @notice Thrown when calling `withdraw` with a ASP or state tree depth greater or equal than the max tree depth
*/
error InvalidTreeDepth();
/**
* @notice Thrown when providing an invalid scope for this pool
@@ -107,11 +110,6 @@ interface IPrivacyPool is IState {
*/
error OnlyOriginalDepositor();
/**
* @notice Thrown when trying to set a state variable as address zero
*/
error ZeroAddress();
/*///////////////////////////////////////////////////////////////
LOGIC
//////////////////////////////////////////////////////////////*/
@@ -150,22 +148,6 @@ interface IPrivacyPool is IState {
* @dev Only callable by the Entrypoint
*/
function windDown() external;
/*///////////////////////////////////////////////////////////////
VIEWS
//////////////////////////////////////////////////////////////*/
/**
* @notice Returns the pool unique identifier
* @return _scope The scope id
*/
function SCOPE() external view returns (uint256 _scope);
/**
* @notice Returns the pool asset
* @return _asset The asset address
*/
function ASSET() external view returns (address _asset);
}
/**
@@ -201,4 +183,9 @@ interface IPrivacyPoolComplex is IPrivacyPool {
* @notice Thrown when sending sending any amount of native asset
*/
error NativeAssetNotAccepted();
/**
* @notice Thrown when trying to set up a complex pool with the native asset
*/
error NativeAssetNotSupported();
}

View File

@@ -10,37 +10,6 @@ import {IVerifier} from 'interfaces/IVerifier.sol';
* @notice Interface for the State contract
*/
interface IState {
/*///////////////////////////////////////////////////////////////
ENUMS
//////////////////////////////////////////////////////////////*/
/**
* @notice Enum representing statuses of a nullifier
*/
enum NullifierStatus {
NONE,
SPENT, // Nullifier is spent
RAGEQUIT_PENDING, // Nullifier is being ragequitted
RAGEQUIT_FINALIZED // Nullifier has been ragequitted
}
/*///////////////////////////////////////////////////////////////
STRUCTS
//////////////////////////////////////////////////////////////*/
/**
* @notice Struct for the deposit data
* @param depositor The address of the depositor
* @param amount The deposited amount
* @param whenRagequitteable The end of the ragequit cooldown period
*/
struct Deposit {
address depositor;
uint256 amount;
uint256 whenRagequitteable;
}
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
@@ -77,15 +46,31 @@ interface IState {
*/
error NotYetRagequitteable();
/**
* @notice Thrown when the max tree depth is reached and no more commitments can be inserted
*/
error MaxTreeDepthReached();
/**
* @notice Thrown when trying to set a state variable as address zero
*/
error ZeroAddress();
/*///////////////////////////////////////////////////////////////
VIEWS
//////////////////////////////////////////////////////////////*/
/**
* @notice Returns the version of the protocol
* @return _version The version string
* @notice Returns the pool unique identifier
* @return _scope The scope id
*/
function VERSION() external view returns (string memory _version);
function SCOPE() external view returns (uint256 _scope);
/**
* @notice Returns the pool asset
* @return _asset The asset address
*/
function ASSET() external view returns (address _asset);
/**
* @notice Returns the root history size for root caching
@@ -93,6 +78,18 @@ interface IState {
*/
function ROOT_HISTORY_SIZE() external view returns (uint32 _size);
/**
* @notice Returns the maximum depth of the state tree
* @dev Merkle tree depth must be capped at a fixed maximum because zero-knowledge circuits
* compile to R1CS (Rank-1 Constraint System) constraints that must be determined at compile time.
* R1CS cannot handle dynamic loops or recursion - all computation paths must be fully "unrolled"
* into a fixed number of constraints. Since each level of the Merkle tree requires its own set
* of constraints for hashing and path verification, we need to set a maximum depth that determines
* the total constraint size of the circuit.
* @return _maxDepth The max depth
*/
function MAX_TREE_DEPTH() external view returns (uint32 _maxDepth);
/**
* @notice Returns the configured Entrypoint contract
* @return _entrypoint The Entrypoint contract
@@ -165,11 +162,6 @@ interface IState {
* @notice Returns the original depositor that generated a label
* @param _label The label
* @return _depositor The original depositor
* @return _amount The amount of deposit
* @return _whenRagequitteable The timestamp on which the user can initiate the ragequit
*/
function deposits(uint256 _label)
external
view
returns (address _depositor, uint256 _amount, uint256 _whenRagequitteable);
function depositors(uint256 _label) external view returns (address _depositor);
}

View File

@@ -235,10 +235,8 @@ contract IntegrationBase is Test {
assertEq(_balance(address(_pool), _params.asset), _poolInitialBalance + _commitment.value, 'Pool balance mismatch');
// Check deposit stored values
(address _depositor, uint256 _value, uint256 _cooldownExpiry) = _pool.deposits(_commitment.label);
address _depositor = _pool.depositors(_commitment.label);
assertEq(_depositor, _params.depositor, 'Incorrect depositor');
assertEq(_value, _commitment.value, 'Incorrect deposit value');
assertEq(_cooldownExpiry, block.timestamp + 1 weeks, 'Incorrect deposit cooldown expiry');
}
/*///////////////////////////////////////////////////////////////
@@ -250,8 +248,7 @@ contract IntegrationBase is Test {
IPrivacyPool _pool = _params.commitment.asset == IERC20(Constants.NATIVE_ASSET) ? _ethPool : _daiPool;
// Build `Withdrawal` object for direct withdrawal
IPrivacyPool.Withdrawal memory _withdrawal =
IPrivacyPool.Withdrawal({processooor: _params.recipient, scope: _pool.SCOPE(), data: ''});
IPrivacyPool.Withdrawal memory _withdrawal = IPrivacyPool.Withdrawal({processooor: _params.recipient, data: ''});
// Withdraw
_commitment = _withdraw(_params.recipient, _pool, _withdrawal, _params, true);
@@ -264,7 +261,6 @@ contract IntegrationBase is Test {
// Build `Withdrawal` object for relayed withdrawal
IPrivacyPool.Withdrawal memory _withdrawal = IPrivacyPool.Withdrawal({
processooor: address(_entrypoint),
scope: _pool.SCOPE(),
data: abi.encode(_params.recipient, _RELAYER, _VETTING_FEE_BPS)
});
@@ -311,13 +307,15 @@ contract IntegrationBase is Test {
})
);
uint256 _scope = _pool.SCOPE();
// Process withdrawal
vm.prank(_caller);
if (_params.revertReason != NONE) vm.expectRevert(_params.revertReason);
if (_direct) {
_pool.withdraw(_withdrawal, _proof);
} else {
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _scope);
}
if (_params.revertReason == NONE) {

View File

@@ -27,8 +27,7 @@ contract IntegrationProofs is IntegrationBase {
vm.prank(_POSTMAN);
_entrypoint.updateRoot(_shadowASPMerkleTree._root(), bytes32('IPFS_HASH'));
_withdrawal =
IPrivacyPool.Withdrawal({processooor: _BOB, scope: _ethPool.SCOPE(), data: abi.encode(_BOB, address(0), 0)});
_withdrawal = IPrivacyPool.Withdrawal({processooor: _BOB, data: abi.encode(_BOB, address(0), 0)});
_context = uint256(keccak256(abi.encode(_withdrawal, _ethPool.SCOPE()))) % SNARK_SCALAR_FIELD;
}

View File

@@ -7,11 +7,14 @@ import {Initializable} from '@oz/proxy/utils/Initializable.sol';
import {ERC20, IERC20} from '@oz/token/ERC20/ERC20.sol';
import {UnsafeUpgrades} from '@upgrades/Upgrades.sol';
import {UUPSUpgradeable} from '@oz-upgradeable/proxy/utils/UUPSUpgradeable.sol';
import {ReentrancyGuardUpgradeable} from '@oz-upgradeable/utils/ReentrancyGuardUpgradeable.sol';
import {IERC1967} from '@oz/interfaces/IERC1967.sol';
import {IPrivacyPool} from 'contracts/PrivacyPool.sol';
import {Constants} from 'contracts/lib/Constants.sol';
import {ProofLib} from 'contracts/lib/ProofLib.sol';
import {IState} from 'interfaces/IState.sol';
import {Entrypoint, IEntrypoint} from 'contracts/Entrypoint.sol';
import {Test} from 'forge-std/Test.sol';
@@ -123,7 +126,7 @@ contract UnitEntrypoint is Test {
modifier givenPoolExists(PoolParams memory _params) {
_assumeFuzzable(_params.pool);
_assumeFuzzable(_params.asset);
_params.vettingFeeBPS = bound(_params.vettingFeeBPS, 0, 10_000);
_params.vettingFeeBPS = bound(_params.vettingFeeBPS, 0, 10_000 - 1);
_params.minDeposit = bound(_params.minDeposit, 1, 100);
_entrypoint.mockPool(_params);
_;
@@ -296,11 +299,9 @@ contract UnitDeposit is UnitEntrypoint {
vm.prank(_depositor);
_entrypoint.deposit{value: _amount}(_precommitment);
// TODO: fix this assertion. somehow the depositor balance is not changing, even though we can see the native asset transfer in the test trace
// assertEq(
// _depositor.balance, _depositorBalanceBefore - _amount, 'Depositor balance should decrease by deposit amount'
// );
// Actually, this ETH should end up in the Pool contract, but as we're mocking the ETH forwarding call, the ETH remains in the Entrypoint
assertEq(
_depositor.balance, _depositorBalanceBefore - _amount, 'Depositor balance should decrease by deposit amount'
);
assertEq(address(_entrypoint).balance, _amount, 'Entrypoint should receive the deposit amount');
}
@@ -453,20 +454,20 @@ contract UnitRelay is UnitEntrypoint {
// Construct withdrawal data with fee distribution details
bytes memory _data = abi.encode(
IEntrypoint.FeeData({
IEntrypoint.RelayData({
recipient: _params.recipient,
feeRecipient: _params.feeRecipient,
relayFeeBPS: _params.feeBPS
})
);
IPrivacyPool.Withdrawal memory _withdrawal =
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), scope: _params.scope, data: _data});
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), data: _data});
// Set up pool and mock necessary interactions
_entrypoint.mockScopeToPool(_params.scope, _params.pool);
uint256 _amountAfterFees = _deductFee(_params.amount, _params.feeBPS);
uint256 _feeAmount = _params.amount - _amountAfterFees;
_mockAndExpect(_params.pool, abi.encodeWithSelector(IPrivacyPool.ASSET.selector), abi.encode(_params.asset));
_mockAndExpect(_params.pool, abi.encodeWithSelector(IState.ASSET.selector), abi.encode(_params.asset));
// Fund the pool with test tokens
deal(_params.asset, _params.pool, _params.amount);
@@ -486,7 +487,7 @@ contract UnitRelay is UnitEntrypoint {
// Execute the relay operation
vm.prank(_params.caller);
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _params.scope);
// Verify final balances reflect correct token distribution
assertEq(
@@ -542,20 +543,21 @@ contract UnitRelay is UnitEntrypoint {
// Construct withdrawal data with fee distribution
bytes memory _data = abi.encode(
IEntrypoint.FeeData({
IEntrypoint.RelayData({
recipient: _params.recipient,
feeRecipient: _params.feeRecipient,
relayFeeBPS: _params.feeBPS
})
);
IPrivacyPool.Withdrawal memory _withdrawal =
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), scope: _params.scope, data: _data});
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), data: _data});
// Setup pool and mock interactions
_entrypoint.mockScopeToPool(_params.scope, _params.pool);
_entrypoint.mockPool(PoolParams(_params.pool, Constants.NATIVE_ASSET, 0, 0));
uint256 _amountAfterFees = _deductFee(_params.amount, _params.feeBPS);
uint256 _feeAmount = _params.amount - _amountAfterFees;
_mockAndExpect(_params.pool, abi.encodeWithSelector(IPrivacyPool.ASSET.selector), abi.encode(_params.asset));
_mockAndExpect(_params.pool, abi.encodeWithSelector(IState.ASSET.selector), abi.encode(_params.asset));
deal(_params.pool, _params.amount);
// Record initial balances for verification
@@ -572,7 +574,7 @@ contract UnitRelay is UnitEntrypoint {
// Execute relay operation
vm.prank(_params.caller);
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _params.scope);
// Verify final balances reflect correct ETH distribution
assertEq(
@@ -616,24 +618,24 @@ contract UnitRelay is UnitEntrypoint {
// Construct withdrawal data with fee distribution
bytes memory _data = abi.encode(
IEntrypoint.FeeData({
IEntrypoint.RelayData({
recipient: _params.recipient,
feeRecipient: _params.feeRecipient,
relayFeeBPS: _params.feeBPS
})
);
IPrivacyPool.Withdrawal memory _withdrawal =
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), scope: _params.scope, data: _data});
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), data: _data});
// Fund entrypoint with more than needed to test faulty pool behavior
deal(address(_entrypoint), _params.amount * 2);
_entrypoint.mockScopeToPool(_params.scope, _params.pool);
_mockAndExpect(_params.pool, abi.encodeWithSelector(IPrivacyPool.ASSET.selector), abi.encode(_params.asset));
_mockAndExpect(_params.pool, abi.encodeWithSelector(IState.ASSET.selector), abi.encode(_params.asset));
// Expect revert due to invalid pool state
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.InvalidPoolState.selector));
vm.prank(_params.caller);
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _params.scope);
}
/**
@@ -641,7 +643,8 @@ contract UnitRelay is UnitEntrypoint {
*/
function test_RelayWhenWithdrawalAmountIsZero(
IPrivacyPool.Withdrawal memory _withdrawal,
ProofLib.WithdrawProof memory _proof
ProofLib.WithdrawProof memory _proof,
uint256 _scope
) external {
// Set withdrawal amount to zero
_proof.pubSignals[2] = 0;
@@ -650,7 +653,7 @@ contract UnitRelay is UnitEntrypoint {
// Expect revert due to invalid withdrawal amount
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.InvalidWithdrawalAmount.selector));
vm.prank(_withdrawal.processooor);
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _scope);
}
/**
@@ -659,7 +662,8 @@ contract UnitRelay is UnitEntrypoint {
function test_RelayWhenPoolNotFound(
address _caller,
IPrivacyPool.Withdrawal memory _withdrawal,
ProofLib.WithdrawProof memory _proof
ProofLib.WithdrawProof memory _proof,
uint256 _scope
) external {
// Ensure non-zero withdrawal amount
vm.assume(_proof.pubSignals[2] != 0);
@@ -668,7 +672,7 @@ contract UnitRelay is UnitEntrypoint {
// Expect revert due to pool not found
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.PoolNotFound.selector));
vm.prank(_caller);
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _scope);
}
/**
@@ -692,19 +696,18 @@ contract UnitRelay is UnitEntrypoint {
// Construct withdrawal data with invalid processooor
bytes memory _data = abi.encode(
IEntrypoint.FeeData({
IEntrypoint.RelayData({
recipient: _params.recipient,
feeRecipient: _params.feeRecipient,
relayFeeBPS: _params.feeBPS
})
);
IPrivacyPool.Withdrawal memory _withdrawal =
IPrivacyPool.Withdrawal({processooor: _processooor, scope: _params.scope, data: _data});
IPrivacyPool.Withdrawal memory _withdrawal = IPrivacyPool.Withdrawal({processooor: _processooor, data: _data});
// Expect revert due to invalid processooor
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.InvalidProcessooor.selector));
vm.prank(_params.caller);
_entrypoint.relay(_withdrawal, _proof);
_entrypoint.relay(_withdrawal, _proof, _params.scope);
}
}
@@ -722,11 +725,12 @@ contract UnitRegisterPool is UnitEntrypoint {
) external givenCallerHasOwnerRole {
// Setup test with valid pool and asset addresses
_assumeFuzzable(_pool);
_vettingFeeBPS = bound(_vettingFeeBPS, 0, 10_000);
_vettingFeeBPS = bound(_vettingFeeBPS, 0, 10_000 - 1);
// Calculate pool scope and mock interactions
uint256 _scope = uint256(keccak256(abi.encodePacked(_pool, block.chainid, _ETH)));
_mockAndExpect(_pool, abi.encodeWithSelector(IPrivacyPool.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_pool, abi.encodeWithSelector(IState.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_pool, abi.encodeWithSelector(IState.ASSET.selector), abi.encode(_ETH));
// Expect pool registration event
vm.expectEmit(address(_entrypoint));
@@ -756,11 +760,12 @@ contract UnitRegisterPool is UnitEntrypoint {
vm.assume(_asset != _ETH);
_assumeFuzzable(_pool);
_assumeFuzzable(_asset);
_vettingFeeBPS = bound(_vettingFeeBPS, 0, 10_000);
_vettingFeeBPS = bound(_vettingFeeBPS, 0, 10_000 - 1);
// Calculate pool scope and mock interactions
uint256 _scope = uint256(keccak256(abi.encodePacked(_pool, block.chainid, _asset)));
_mockAndExpect(_pool, abi.encodeWithSelector(IPrivacyPool.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_pool, abi.encodeWithSelector(IState.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_pool, abi.encodeWithSelector(IState.ASSET.selector), abi.encode(_asset));
// Mock ERC20 approval for non-ETH assets
_mockAndExpect(_asset, abi.encodeWithSelector(IERC20.approve.selector, _pool, type(uint256).max), abi.encode(true));
@@ -810,12 +815,12 @@ contract UnitRegisterPool is UnitEntrypoint {
// Setup test with valid addresses and parameters
_assumeFuzzable(_pool);
_assumeFuzzable(_asset);
vm.assume(_vettingFeeBPS <= 10_000);
vm.assume(_vettingFeeBPS < 10_000);
// Mock existing pool with same scope
uint256 _scope = uint256(keccak256(abi.encodePacked(_pool, block.chainid, _asset)));
_entrypoint.mockScopeToPool(_scope, _pool);
_mockAndExpect(_pool, abi.encodeWithSelector(IPrivacyPool.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_pool, abi.encodeWithSelector(IState.SCOPE.selector), abi.encode(_scope));
// Expect revert when trying to register pool with existing scope
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.ScopePoolAlreadyRegistered.selector));
@@ -863,7 +868,7 @@ contract UnitRemovePool is UnitEntrypoint {
{
_params.asset = _ETH;
// Mock pool scope and interactions
_mockAndExpect(_params.pool, abi.encodeWithSelector(IPrivacyPool.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_params.pool, abi.encodeWithSelector(IState.SCOPE.selector), abi.encode(_scope));
// Expect pool removal event
vm.expectEmit(address(_entrypoint));
@@ -890,7 +895,7 @@ contract UnitRemovePool is UnitEntrypoint {
) external givenCallerHasOwnerRole givenPoolExists(_params) {
vm.assume(_params.asset != _ETH);
// Mock pool scope and interactions
_mockAndExpect(_params.pool, abi.encodeWithSelector(IPrivacyPool.SCOPE.selector), abi.encode(_scope));
_mockAndExpect(_params.pool, abi.encodeWithSelector(IState.SCOPE.selector), abi.encode(_scope));
// Mock ERC20 approval reset for non-ETH assets
_mockAndExpect(_params.asset, abi.encodeWithSelector(IERC20.approve.selector, _params.pool, 0), abi.encode(true));
@@ -955,7 +960,7 @@ contract UnitUpdatePoolConfiguration is UnitEntrypoint {
assertEq(_minDeposit, _params.minDeposit, 'Retrieved minimum deposit should match input');
assertEq(_vettingFeeBPS, _params.vettingFeeBPS, 'Retrieved vetting fee should match input');
_newParams.vettingFeeBPS = bound(_newParams.vettingFeeBPS, 0, 10_000);
_newParams.vettingFeeBPS = bound(_newParams.vettingFeeBPS, 0, 10_000 - 1);
// Expect configuration update event
vm.expectEmit(address(_entrypoint));
@@ -980,7 +985,7 @@ contract UnitUpdatePoolConfiguration is UnitEntrypoint {
uint256 _minDeposit,
uint256 _vettingFeeBPS
) external givenCallerHasOwnerRole {
_vettingFeeBPS = bound(_vettingFeeBPS, 0, 10_000);
_vettingFeeBPS = bound(_vettingFeeBPS, 0, 10_000 - 1);
// Expect revert when trying to update non-existent pool
vm.expectRevert(abi.encodeWithSelector(IEntrypoint.PoolNotFound.selector));
_entrypoint.updatePoolConfiguration(IERC20(_asset), _minDeposit, _vettingFeeBPS);
@@ -1064,6 +1069,7 @@ contract UnitWithdrawFees is UnitEntrypoint {
_assumeFuzzable(_recipient);
vm.assume(_recipient != address(10));
vm.assume(_recipient != address(_entrypoint));
vm.assume(_recipient != address(_impl));
vm.assume(_balance != 0);
vm.deal(address(_entrypoint), _balance);
@@ -1187,29 +1193,20 @@ contract UnitViewMethods is UnitEntrypoint {
}
}
/**
* @notice Dummy contract for upgrades testing
*/
contract Implementation is UUPSUpgradeable {
bytes32 private immutable salt;
constructor(bytes32 _salt) {
salt = _salt;
}
fallback() external {}
function _authorizeUpgrade(address newImplementation) internal override {}
}
/**
* @notice Unit tests for upgrading the Entrypoint contract
*/
contract UnitUpgrades is UnitEntrypoint {
function test_upgradeEntrypoint(bytes32 _salt, bytes calldata _data) public {
address _newImplementation = address(new Implementation(_salt));
vm.expectCall(_newImplementation, abi.encodeWithSignature('proxiableUUID()'));
/**
* @notice Test that the Entrypoint properly upgrades to a new implementation
*/
function test_upgradeEntrypoint(address _newImplementation, bytes calldata _data) public {
_assumeFuzzable(_newImplementation);
_mockAndExpect(
_newImplementation,
abi.encodeWithSignature('proxiableUUID()'),
abi.encode(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)
);
if (keccak256(_data) != keccak256('')) {
_mockAndExpect(_newImplementation, _data, abi.encode());
@@ -1222,3 +1219,202 @@ contract UnitUpgrades is UnitEntrypoint {
_entrypoint.upgradeToAndCall(_newImplementation, _data);
}
}
/**
* @notice Unit tests for the `receive` method
*/
contract UnitReceive is UnitEntrypoint {
/**
* @notice Test that the Entrypoint doesn't accept native asset from any other address than the native pool
*/
function test_nativeAssetTransferToEntrypointFails(
address _caller,
uint256 _amount,
PoolParams memory _params
) external givenPoolExists(PoolParams({pool: _params.pool, asset: _ETH, minDeposit: 0, vettingFeeBPS: 0})) {
// Config pool
(IPrivacyPool _nativePool,,) = _entrypoint.assetConfig(IERC20(_ETH));
// Filter pool address
vm.assume(_caller != address(_nativePool));
vm.deal(_caller, _amount);
// Check it reverts when sending native asset
vm.expectRevert(IEntrypoint.NativeAssetNotAccepted.selector);
vm.prank(_caller);
payable(address(_entrypoint)).transfer(_amount);
}
}
/**
* @notice Unit tests for Entrypoint's role based access configuration
*/
contract UnitAccessControl is UnitEntrypoint {
bytes32 public constant OWNER_ROLE = 0x6270edb7c868f86fda4adedba75108201087268ea345934db8bad688e1feb91b;
bytes32 public constant ASP_POSTMAN = 0xfc84ade01695dae2ade01aa4226dc40bdceaf9d5dbd3bf8630b1dd5af195bbc5;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @notice Test that the OWNER_ROLE can manage other roles
*/
function test_ownerRole(address _notOwner, address _account) public {
// Not owner can't manager OWNER_ROLE
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _notOwner, OWNER_ROLE)
);
vm.prank(_notOwner);
_entrypoint.grantRole(OWNER_ROLE, _account);
// Not owner can't manager ASP_POSTMAN role
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _notOwner, OWNER_ROLE)
);
vm.prank(_notOwner);
_entrypoint.grantRole(ASP_POSTMAN, _account);
// Not owner can't manager DEFAULT_ADMIN_ROLE
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _notOwner, OWNER_ROLE)
);
vm.prank(_notOwner);
_entrypoint.grantRole(DEFAULT_ADMIN_ROLE, _account);
// Owner can manage OWNER_ROLE
vm.prank(_OWNER);
_entrypoint.grantRole(OWNER_ROLE, _account);
assertTrue(_entrypoint.hasRole(OWNER_ROLE, _account), 'Account must have owner role');
// Owner can manage ASP_POSTMAN role
vm.prank(_OWNER);
_entrypoint.grantRole(ASP_POSTMAN, _account);
assertTrue(_entrypoint.hasRole(ASP_POSTMAN, _account), 'Account must have postman role');
// Owner can manage DEFAULT_ADMIN_ROLE
vm.prank(_OWNER);
_entrypoint.grantRole(DEFAULT_ADMIN_ROLE, _account);
assertTrue(_entrypoint.hasRole(DEFAULT_ADMIN_ROLE, _account), 'Account must have default admin role');
}
/**
* @notice Test that the ASP_POSTMAN role can't manage other roles
*/
function test_postmanRole(address _account) public {
// Postman can't manage OWNER_ROLE
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _POSTMAN, OWNER_ROLE)
);
vm.prank(_POSTMAN);
_entrypoint.grantRole(OWNER_ROLE, _account);
// Postman can't manage ASP_POSTMAN role
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _POSTMAN, OWNER_ROLE)
);
vm.prank(_POSTMAN);
_entrypoint.grantRole(ASP_POSTMAN, _account);
// Postman can't manage DEFAULT_ADMIN_ROLE
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _POSTMAN, OWNER_ROLE)
);
vm.prank(_POSTMAN);
_entrypoint.grantRole(DEFAULT_ADMIN_ROLE, _account);
}
/**
* @notice Test that the DEFAULT_ADMIN_ROLE can't manage other roles
*/
function test_defaultAdminRole(address _defaultAdmin, address _account) public {
vm.prank(_OWNER);
_entrypoint.grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
// DEFAULT_ADMIN_ROLE can't manage OWNER_ROLE
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _defaultAdmin, OWNER_ROLE)
);
vm.prank(_defaultAdmin);
_entrypoint.grantRole(OWNER_ROLE, _account);
// DEFAULT_ADMIN_ROLE can't manage ASP_POSTMAN
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _defaultAdmin, OWNER_ROLE)
);
vm.prank(_defaultAdmin);
_entrypoint.grantRole(ASP_POSTMAN, _account);
// DEFAULT_ADMIN_ROLE can't manage DEFAULT_ADMIN_ROLE
vm.expectRevert(
abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, _defaultAdmin, OWNER_ROLE)
);
vm.prank(_defaultAdmin);
_entrypoint.grantRole(DEFAULT_ADMIN_ROLE, _account);
}
}
/**
* @notice Unit tests for checking reentrancy protection
*/
contract UnitReentrancy is UnitEntrypoint {
/**
* @notice Test that the Entrypoint properly upgrades to a new implementation
* @dev If you run the test with maximum verbosity you can see in the traces that the reentrant call
* properly reverts with `error ReentrancyGuardUpgradeable.ReentrancyGuardReentrantCall`, but since
* the native asset transfer call from the Entrypoint is a low-level `call`, the error doesn't bubble up
* and we assert the revert with the custom `error IEntrypoint.NativeAssetTransferFailed`.
* It is also checked that the Entrypoint receives the reentrant `deposit` call.
*/
function test_reentrantRelay(RelayParams memory _params, ProofLib.WithdrawProof memory _proof) external {
// Deploy attacker contract
Attacker _attacker = new Attacker();
// Setup test with valid recipients and amounts
////////////////////////////////////////// RELAY SETUP : IGNORE ////////////////////////////////////////
_assumeFuzzable(_params.recipient);
_assumeFuzzable(_params.feeRecipient);
vm.assume(_params.recipient != address(10));
vm.assume(_params.feeRecipient != address(10));
vm.assume(_params.recipient != _params.feeRecipient);
vm.assume(_params.recipient != address(_entrypoint));
vm.assume(_params.feeRecipient != address(_entrypoint));
vm.assume(_params.amount != 0);
_params.asset = _ETH;
_params.pool = address(new PrivacyPoolETHForTest());
_params.feeBPS = bound(_params.feeBPS, 0, 10_000);
_params.amount = bound(_params.amount, 1, 1e30);
_proof.pubSignals[2] = _params.amount;
bytes memory _data = abi.encode(
IEntrypoint.RelayData({
recipient: address(_attacker), // <---- setting the Attacker contract as recipient
feeRecipient: _params.feeRecipient,
relayFeeBPS: _params.feeBPS
})
);
IPrivacyPool.Withdrawal memory _withdrawal =
IPrivacyPool.Withdrawal({processooor: address(_entrypoint), data: _data});
_entrypoint.mockScopeToPool(_params.scope, _params.pool);
_entrypoint.mockPool(PoolParams(_params.pool, Constants.NATIVE_ASSET, 0, 0));
_mockAndExpect(_params.pool, abi.encodeWithSelector(IState.ASSET.selector), abi.encode(_params.asset));
deal(_params.pool, _params.amount);
////////////////////////////////////////// RELAY SETUP : IGNORE ////////////////////////////////////////
// Expect the Attacker contract calling deposit on the Entrypoint
vm.expectCall(
address(_entrypoint), abi.encodeWithSignature('deposit(uint256)', uint256(keccak256('precommitment')))
);
// Revert when trying to relay
vm.expectRevert(IEntrypoint.NativeAssetTransferFailed.selector);
vm.prank(_params.caller);
_entrypoint.relay(_withdrawal, _proof, _params.scope);
}
}
/**
* @notice Helper contract for testing reetrancy checks
*/
contract Attacker {
fallback() external payable {
Entrypoint(payable(msg.sender)).deposit(uint256(keccak256('precommitment')));
}
}

View File

@@ -4,6 +4,8 @@ Entrypoint::constructor
Entrypoint::initialize
├── Given valid owner and postman
│ ├── It initializes upgradeable contracts
│ ├── It initializes roles
│ ├── It grants owner role
│ └── It grants postman role
└── When already initialized
@@ -24,45 +26,55 @@ Entrypoint::updateRoot
└── It reverts with AccessControlUnauthorizedAccount
Entrypoint::deposit (ETH)
├── Given pool exists
│ ├── Given value meets minimum
│ │ ├── It deducts correct fees
│ │ ├── It forwards ETH to pool
│ │ ├── It maintains contract balance
│ │ ── It emits Deposited event
└── When value below minimum
└── It reverts with MinimumDepositAmount
└── When pool not found
└── It reverts with PoolNotFound
├── Given no reentrant call
│ ├── Given pool exists
│ │ ├── Given value meets minimum
│ │ ├── It deducts correct fees
│ │ ├── It forwards ETH to pool
│ │ │ ├── It maintains contract balance
│ │ └── It emits Deposited event
└── When value below minimum
│ │ └── It reverts with MinimumDepositAmount
└── When pool not found
│ └── It reverts with PoolNotFound
└── When reentrant call
└── It reverts
Entrypoint::deposit (ERC20)
├── Given pool exists
│ ├── Given value meets minimum
│ │ ├── It transfers tokens from sender
│ │ ├── It deducts correct fees
│ │ ├── It deposits to pool
│ │ ── It emits Deposited event
└── When value below minimum
└── It reverts with MinimumDepositAmount
└── When pool not found
└── It reverts with PoolNotFound
├── Given no reentrant call
│ ├── Given pool exists
│ │ ├── Given value meets minimum
│ │ ├── It transfers tokens from sender
│ │ ├── It deducts correct fees
│ │ │ ├── It deposits to pool
│ │ └── It emits Deposited event
└── When value below minimum
│ │ └── It reverts with MinimumDepositAmount
└── When pool not found
│ └── It reverts with PoolNotFound
└── When reentrant call
└── It reverts
Entrypoint::relay
├── Given pool exists
│ ├── Given valid processooor
│ │ ├── Given valid withdrawal and proof
│ │ │ ├── It processes withdrawal
│ │ │ ├── It transfers correct amounts
│ │ │ ├── It maintains pool balance
│ │ │ └── It emits WithdrawalRelayed event
│ │ ├── When withdrawal amount is zero
│ │ │ └── It reverts with InvalidWithdrawalAmount
│ │ └── When pool state is invalid
│ │ └── It reverts with InvalidPoolState
│ └── When invalid processooor
│ └── It reverts with InvalidProcessooor
└── When pool not found
└── It reverts with PoolNotFound
├── Given no reentrant call
│ ├── Given pool exists
│ │ ├── Given valid processooor
│ │ │ ├── Given valid withdrawal and proof
│ │ │ │ ├── Given withdrawal amount is not zero
│ │ │ │ │ ├── It processes withdrawal
│ │ │ │ │ ├── It transfers correct amounts
│ │ │ │ │ ├── It maintains pool balance
│ │ │ │ │ └── It emits WithdrawalRelayed event
│ │ │ │ └── When withdrawal amount is zero
│ │ │ │ └── It reverts with InvalidWithdrawalAmount
│ │ │ └── When pool state is invalid
│ │ │ └── It reverts with InvalidPoolState
│ │ └── When invalid processooor
│ │ └── It reverts with InvalidProcessooor
│ └── When pool not found
│ └── It reverts with PoolNotFound
└── When reentrant call
└── It reverts
Entrypoint::registerPool
├── Given caller has owner role
@@ -93,9 +105,12 @@ Entrypoint::removePool
Entrypoint::updatePoolConfiguration
├── Given caller has owner role
│ ├── Given valid pool and configuration
│ │ ├── It updates minimum deposit amount
│ │ ├── It updates fee basis points
│ │ ── It emits PoolConfigurationUpdated event
│ │ ├── Given fee BPS less than 100%
│ │ ├── It updates minimum deposit amount
│ │ │ ├── It updates fee basis points
│ │ │ └── It emits PoolConfigurationUpdated event
│ │ └── When fee BPS is 100% or greater
│ │ └── It reverts with InvalidFeeBPS
│ └── When pool not found
│ └── It reverts with PoolNotFound
└── When caller lacks owner role
@@ -113,18 +128,21 @@ Entrypoint::windDownPool
Entrypoint::withdrawFees
├── Given caller has owner role
│ ├── Given asset is ETH
│ │ ├── When ETH balance exists
│ │ │ ├── It transfers full balance
│ │ │ ── It emits FeesWithdrawn event
│ │ └── When ETH transfer fails
│ │ └── It reverts with ETHTransferFailed
└── Given asset is ERC20
── When token balance exists
├── It transfers full balance
│ │ ── It emits FeesWithdrawn event
└── When token transfer fails
└── It reverts
│ ├── Given no reentrant call
│ │ ├── Given asset is native asset
│ │ │ ├── When balance exists
│ │ │ │ ├── It transfers full balance
│ │ │ │ └── It emits FeesWithdrawn event
│ │ └── When transfer fails
│ │ └── It reverts with NativeAssetTransferFailed
── Given asset is ERC20
├── When token balance exists
│ │ ── It transfers full balance
│ └── It emits FeesWithdrawn event
└── When token transfer fails
│ │ └── It reverts
│ └── When reentrant call
│ └── It reverts
└── When caller lacks owner role
└── It reverts with AccessControlUnauthorizedAccount

View File

@@ -57,8 +57,8 @@ contract PoolForTest is PrivacyPool {
}
function mockDeposit(address _depositor, uint256 _label) external {
deposits[_label] = Deposit(_depositor, 1, block.timestamp + 1 weeks);
deposits[_label] = Deposit(_depositor, 1, block.timestamp + 1 weeks);
depositors[_label] = _depositor;
depositors[_label] = _depositor;
}
function mockNullifierStatus(uint256 _nullifierHash, bool _spent) external {
@@ -72,6 +72,18 @@ contract PoolForTest is PrivacyPool {
function insertLeaf(uint256 _leaf) external returns (uint256 _root) {
_root = _merkleTree._insert(_leaf);
}
function mockTreeDepth(uint256 _depth) external {
_merkleTree.depth = _depth;
}
function mockTreeSize(uint256 _size) external {
_merkleTree.size = _size;
}
function mockCurrentRoot(uint256 _root) external {
_merkleTree.sideNodes[_merkleTree.depth] = _root;
}
}
/**
@@ -106,12 +118,25 @@ contract UnitPrivacyPool is Test {
}
modifier givenValidProof(IPrivacyPool.Withdrawal memory _w, ProofLib.WithdrawProof memory _p) {
_p.pubSignals[2] = bound(_p.pubSignals[2], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
_p.pubSignals[3] = bound(_p.pubSignals[3], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
_p.pubSignals[5] = bound(_p.pubSignals[5], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
_p.pubSignals[1] = bound(_p.pubSignals[6], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
_p.pubSignals[0] = bound(_p.pubSignals[7], 1, Constants.SNARK_SCALAR_FIELD - 1);
// New commitment hash
_p.pubSignals[0] = bound(_p.pubSignals[0], 1, Constants.SNARK_SCALAR_FIELD - 1);
// Existing nullifier hash
_p.pubSignals[1] = bound(_p.pubSignals[1], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
// Withdrawn value
_p.pubSignals[2] = bound(_p.pubSignals[2], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
// State root
_p.pubSignals[3] = bound(_p.pubSignals[3], 1, type(uint256).max) % Constants.SNARK_SCALAR_FIELD;
// State tree depth
_p.pubSignals[4] = bound(_p.pubSignals[4], 1, 32);
// ASP tree depth
_p.pubSignals[6] = bound(_p.pubSignals[6], 1, 32);
// Context
_p.pubSignals[7] = uint256(keccak256(abi.encode(_w, _scope))) % Constants.SNARK_SCALAR_FIELD;
_;
@@ -230,13 +255,13 @@ contract UnitConstructor is UnitPrivacyPool {
address _ragequitVerifier,
address _asset
) external {
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new PoolForTest(address(0), _withdrawalVerifier, _ragequitVerifier, _asset);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new PoolForTest(_entrypoint, address(0), _ragequitVerifier, _asset);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new PoolForTest(_entrypoint, _withdrawalVerifier, address(0), _asset);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new PoolForTest(_entrypoint, _withdrawalVerifier, _ragequitVerifier, address(0));
}
}
@@ -275,9 +300,8 @@ contract UnitDeposit is UnitPrivacyPool {
_pool.deposit(_depositor, _amount, _precommitmentHash);
// Verify deposit was recorded correctly
(address _retrievedDepositor, uint256 _retrievedAmount,) = _pool.deposits(_label);
address _retrievedDepositor = _pool.depositors(_label);
assertEq(_retrievedDepositor, _depositor);
assertEq(_retrievedAmount, _amount);
assertEq(_pool.nonce(), _nonce + 1);
}
@@ -312,9 +336,8 @@ contract UnitDeposit is UnitPrivacyPool {
emit IPrivacyPool.Deposited(_depositor, _commitment, _label, _amount, _newRoot);
_pool.deposit(_depositor, _amount, _precommitmentHash);
(address _retrievedDepositor, uint256 _retrievedAmount,) = _pool.deposits(_label);
address _retrievedDepositor = _pool.depositors(_label);
assertEq(_retrievedDepositor, _depositor);
assertEq(_retrievedAmount, _amount);
assertEq(_pool.nonce(), _nonce + 1);
}
@@ -342,9 +365,8 @@ contract UnitDeposit is UnitPrivacyPool {
emit IPrivacyPool.Deposited(_depositor, _commitment, _label, _amount, _newRoot);
_pool.deposit(_depositor, _amount, _precommitmentHash);
(address _retrievedDepositor, uint256 _retrievedAmount,) = _pool.deposits(_label);
address _retrievedDepositor = _pool.depositors(_label);
assertEq(_retrievedDepositor, _depositor);
assertEq(_retrievedAmount, _amount);
assertEq(_pool.nonce(), _nonce + 1);
}
@@ -396,6 +418,27 @@ contract UnitDeposit is UnitPrivacyPool {
vm.prank(_caller);
_pool.deposit(_depositor, _amount, _precommitmentHash);
}
/**
* @notice Test that deposit reverts when max tree depth is reached
*/
function test_DepositWhenMaxTreeDepthReached(
address _depositor,
uint256 _amount,
uint256 _precommitmentHash
) external givenCallerIsEntrypoint givenPoolIsActive {
vm.assume(_depositor != address(0));
vm.assume(_amount > 0);
vm.assume(_precommitmentHash != 0);
// Mock tree at max capacity
_pool.mockTreeDepth(32);
_pool.mockTreeSize(2 ** 32);
// Attempt deposit that would exceed max depth
vm.expectRevert(IState.MaxTreeDepthReached.selector);
_pool.deposit(_depositor, _amount, _precommitmentHash);
}
}
/**
@@ -433,6 +476,32 @@ contract UnitWithdraw is UnitPrivacyPool {
assertTrue(_pool.nullifierHashes(_p.existingNullifierHash()), 'Nullifier should be spent');
}
function test_WithdrawWhenTreeIsFull(
IPrivacyPool.Withdrawal memory _w,
ProofLib.WithdrawProof memory _p
)
external
givenCallerIsProcessooor(_w.processooor)
givenValidProof(_w, _p)
givenKnownStateRoot(_p.stateRoot())
givenLatestASPRoot(_p.ASPRoot())
{
// Tree is full
_pool.mockTreeSize(2 ** 32);
_pool.mockTreeDepth(32);
_mockAndExpect(
_WITHDRAWAL_VERIFIER,
abi.encodeWithSignature(
'verifyProof(uint256[2],uint256[2][2],uint256[2],uint256[8])', _p.pA, _p.pB, _p.pC, _p.pubSignals
),
abi.encode(true)
);
vm.expectRevert(IState.MaxTreeDepthReached.selector);
_pool.withdraw(_w, _p);
}
function test_WithdrawWhenWithdrawingNullifierAlreadySpent(
IPrivacyPool.Withdrawal memory _w,
ProofLib.WithdrawProof memory _p
@@ -519,10 +588,30 @@ contract UnitWithdraw is UnitPrivacyPool {
vm.assume(_caller != _w.processooor);
// Expect revert due to invalid processooor
vm.expectRevert(IPrivacyPool.InvalidProcesooor.selector);
vm.expectRevert(IPrivacyPool.InvalidProcessooor.selector);
vm.prank(_caller);
_pool.withdraw(_w, _p);
}
/**
* @notice Test that withdraw reverts when tree depths are invalid
*/
function test_WithdrawWhenTreeDepthsInvalid(
IPrivacyPool.Withdrawal memory _w,
ProofLib.WithdrawProof memory _p
) external givenCallerIsProcessooor(_w.processooor) givenValidProof(_w, _p) {
// Set the state tree depth
_p.pubSignals[4] = 33;
vm.expectRevert(IPrivacyPool.InvalidTreeDepth.selector);
_pool.withdraw(_w, _p);
// Test ASP tree depth > MAX_TREE_DEPTH
_p.pubSignals[4] = 32; // Reset to valid depth
_p.pubSignals[6] = 33;
vm.expectRevert(IPrivacyPool.InvalidTreeDepth.selector);
_pool.withdraw(_w, _p);
}
}
/**
@@ -665,3 +754,32 @@ contract UnitWindDown is UnitPrivacyPool {
_pool.windDown();
}
}
/**
* @notice Unit tests for the state view methods
*/
contract UnitStateViews is UnitPrivacyPool {
/**
* @notice Test for getting the current state root
*/
function test_currentRoot(uint256 _root) external {
_pool.mockCurrentRoot(_root);
assertEq(_pool.currentRoot(), _root);
}
/**
* @notice Test for getting the current tree depth
*/
function test_currentTreeDepth(uint256 _depth) external {
_pool.mockTreeDepth(_depth);
assertEq(_pool.currentTreeDepth(), _depth);
}
/**
* @notice Test for getting the current tree size
*/
function test_currentTreeSize(uint256 _size) external {
_pool.mockTreeSize(_size);
assertEq(_pool.currentTreeSize(), _size);
}
}

View File

@@ -15,7 +15,6 @@ PrivacyPool::deposit
│ │ │ ├── It increments global nonce
│ │ │ ├── It computes label as keccak of scope and nonce
│ │ │ ├── It maps label to depositor address
│ │ │ ├── It sets ragequit cooldown period
│ │ │ ├── It computes commitment hash correctly
│ │ │ ├── It inserts commitment in merkle tree
│ │ │ ├── It handles asset transfer
@@ -34,14 +33,17 @@ PrivacyPool::withdraw
│ ├── Given valid proof
│ │ ├── Given known state root
│ │ │ ├── Given latest ASP root
│ │ │ │ ├── When withdrawing nonzero amount
│ │ │ │ │ ├── It verifies proof with verifier
│ │ │ │ │ ├── It spends nullifier hash
│ │ │ │ │ ├── It inserts new commitment
│ │ │ │ │ ├── It transfers value to processooor
│ │ │ │ │ ── It emits Withdrawn event
│ │ │ │ └── When nullifier already spent
│ │ │ │ └── It reverts with NullifierAlreadySpent
│ │ │ │ ├── Given valid tree depths
│ │ │ │ │ ├── When withdrawing nonzero amount
│ │ │ │ │ ├── It verifies proof with verifier
│ │ │ │ │ ├── It spends nullifier hash
│ │ │ │ │ ├── It inserts new commitment
│ │ │ │ │ │ ├── It transfers value to processooor
│ │ │ │ │ │ └── It emits Withdrawn event
│ │ │ │ └── When nullifier already spent
│ │ │ │ │ └── It reverts with NullifierAlreadySpent
│ │ │ │ └── When tree depths exceed maximum
│ │ │ │ └── It reverts with InvalidTreeDepth
│ │ │ └── When ASP root is outdated
│ │ │ └── It reverts with IncorrectASPRoot
│ │ └── When state root unknown
@@ -49,7 +51,7 @@ PrivacyPool::withdraw
│ └── When proof context mismatches
│ └── It reverts with ContextMismatch
└── When caller is not processooor
└── It reverts with InvalidProcesooor
└── It reverts with InvalidProcessooor
PrivacyPool::ragequit
├── Given caller is original depositor

View File

@@ -6,6 +6,7 @@ import {Test} from 'forge-std/Test.sol';
import {IERC20} from '@oz/token/ERC20/IERC20.sol';
import {IPrivacyPool} from 'interfaces/IPrivacyPool.sol';
import {IState} from 'interfaces/IState.sol';
import {Constants} from 'test/helper/Constants.sol';
@@ -76,7 +77,7 @@ contract UnitConstructor is UnitPrivacyPoolComplex {
) external {
vm.assume(
_entrypoint != address(0) && _withdrawalVerifier != address(0) && _ragequitVerifier != address(0)
&& _asset != address(0)
&& _asset != address(0) && _asset != Constants.NATIVE_ASSET
);
_pool = new ComplexPoolForTest(_entrypoint, _withdrawalVerifier, _ragequitVerifier, _asset);
@@ -104,15 +105,31 @@ contract UnitConstructor is UnitPrivacyPoolComplex {
address _ragequitVerifier,
address _asset
) external {
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new ComplexPoolForTest(address(0), _withdrawalVerifier, _ragequitVerifier, _asset);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new ComplexPoolForTest(_entrypoint, address(0), _ragequitVerifier, _asset);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new ComplexPoolForTest(_entrypoint, _withdrawalVerifier, address(0), _asset);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new ComplexPoolForTest(_entrypoint, _withdrawalVerifier, _ragequitVerifier, address(0));
}
/**
* @notice Test that constructor reverts when native asset is used
*/
function test_ConstructorWhenAssetIsNative(
address _entrypoint,
address _withdrawalVerifier,
address _ragequitVerifier
) external {
vm.assume(_entrypoint != address(0));
vm.assume(_withdrawalVerifier != address(0));
vm.assume(_ragequitVerifier != address(0));
vm.expectRevert(IPrivacyPoolComplex.NativeAssetNotSupported.selector);
new ComplexPoolForTest(_entrypoint, _withdrawalVerifier, _ragequitVerifier, Constants.NATIVE_ASSET);
}
}
contract UnitPull is UnitPrivacyPoolComplex {

View File

@@ -1,16 +1,18 @@
PrivacyPoolComplex::constructor
├── Given valid addresses
│ ├── It sets asset address
│ ├── It computes scope from chain id and asset
│ ├── It initializes base state contract
│ └── It enables deposits by default
│ ├── Given non-native asset
│ ├── It sets asset address
│ ├── It computes scope from chain id and asset
│ └── It initializes base state contract
│ └── When asset is native
│ └── It reverts with NativeAssetNotSupported
└── When any address is zero
└── It reverts with ZeroAddress
PrivacyPoolComplex::_pull
├── Given msg.value is not zero
│ └── It reverts with NativeAssetNotAccepted
── Given msg.value is zero
── Given msg.value is zero
└── It transfers _amount of asset from _sender to the pool
PrivacyPoolComplex::_push

View File

@@ -6,6 +6,7 @@ import {Test} from 'forge-std/Test.sol';
import {Constants} from 'test/helper/Constants.sol';
import {IPrivacyPool} from 'interfaces/IPrivacyPool.sol';
import {IState} from 'interfaces/IState.sol';
/**
* @notice Test contract for the PrivacyPoolSimple
@@ -96,13 +97,13 @@ contract UnitConstructor is UnitPrivacyPoolSimple {
address _withdrawalVerifier,
address _ragequitVerifier
) external {
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new SimplePoolForTest(address(0), _withdrawalVerifier, _ragequitVerifier);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new SimplePoolForTest(_entrypoint, address(0), _ragequitVerifier);
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new SimplePoolForTest(_entrypoint, _withdrawalVerifier, address(0));
vm.expectRevert(IPrivacyPool.ZeroAddress.selector);
vm.expectRevert(IState.ZeroAddress.selector);
new SimplePoolForTest(address(0), _withdrawalVerifier, _ragequitVerifier);
}
}

View File

@@ -1,9 +1,8 @@
PrivacyPool::constructor
PrivacyPoolSimple::constructor
├── Given valid addresses
│ ├── It sets asset address
│ ├── It sets asset address to native asset
│ ├── It computes scope from chain id and asset
── It initializes base state contract
│ └── It enables deposits by default
── It initializes base state contract
└── When any address is zero
└── It reverts with ZeroAddress
@@ -16,4 +15,4 @@ PrivacyPoolSimple::_pull
PrivacyPoolSimple::_push
├── It sends _amount of native asset to _recipient
└── When the transfer fails
└── It reverts with FailedToSendETH
└── It reverts with FailedToSendNativeAsset

View File

@@ -758,15 +758,15 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.2.tgz#3e5321a2ecdd0b206064356798c21225b6ec7105"
integrity sha512-0MmkHSHiW2NRFiT9/r5Lu4eJq5UJ4/tzlOgYXNAIj/ONkQTVnz22pLxDvp4C4uZ9he7ZFvGn3Driptn1/iU7tQ==
"@openzeppelin/contracts@^5.1.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.2.0.tgz#bd020694218202b811b0ea3eec07277814c658da"
integrity sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA==
"@openzeppelin/contracts@5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.1.0.tgz#4e61162f2a2bf414c4e10c45eca98ce5f1aadbd4"
integrity sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA==
"@openzeppelin/foundry-upgrades@^0.3.6":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@openzeppelin/foundry-upgrades/-/foundry-upgrades-0.3.8.tgz#134227b824b17b426c89bc0de6f7905c521dccff"
integrity sha512-K3EscnnoRudDzG/359cAR9niivnuFILUoSQQcaBjXvyZUuH/DXIM3Cia9Ni8xJVyvi13hvUFUVD+2suB+dT54w==
"@openzeppelin/foundry-upgrades@0.3.6":
version "0.3.6"
resolved "https://registry.yarnpkg.com/@openzeppelin/foundry-upgrades/-/foundry-upgrades-0.3.6.tgz#bba9249e206c053326802742ced05596f2ea6ccb"
integrity sha512-qIRYAw/Kh9AjxAr4kVuTwqLtJkkamRB65ZEJJQBlFADbMJyNQgCX4XPOh3MUqIGgYl4YtG/GzhZAkCedxfv0SQ==
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
@@ -1500,7 +1500,7 @@
buffer "6.0.3"
poseidon-lite "0.3.0"
"@zk-kit/lean-imt.sol@^2.0.0":
"@zk-kit/lean-imt.sol@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@zk-kit/lean-imt.sol/-/lean-imt.sol-2.0.0.tgz#4b0aee47854b5844455f9361396062139416e12b"
integrity sha512-e9pAm+IXveLPy7b1h05ipIo6U44vp8g/2E+Ocx3PIloMu7lgTXFkIeZj/qZ/iLgEMsF74T0dsg7aVIT0B0nsDA==
@@ -3074,9 +3074,9 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
"forge-std@github:foundry-rs/forge-std#1.9.2":
version "1.9.2"
resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/1714bee72e286e73f76e320d110e0eaf5c4e649d"
"forge-std@github:foundry-rs/forge-std#1.9.6":
version "1.9.6"
resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/3b20d60d14b343ee4f908cb8079495c07f5e8981"
form-data@^4.0.0:
version "4.0.1"