mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-09 01:17:58 -05:00
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:
2
.github/workflows/contracts.yml
vendored
2
.github/workflows/contracts.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 ////////////////////
|
||||
|
||||
|
||||
3
packages/contracts/.gitignore
vendored
3
packages/contracts/.gitignore
vendored
@@ -23,3 +23,6 @@ crytic-export
|
||||
|
||||
# Echidna corpus
|
||||
test/invariants/fuzz/echidna_coverage
|
||||
|
||||
# Coverage files
|
||||
lcov.info
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
24
yarn.lock
24
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user