From f3856a787df8762d48d6a57d8f8562a8567ef03d Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Wed, 29 Mar 2023 23:17:15 +0300 Subject: [PATCH 01/11] first version --- src/interfaces/IPermit2.sol | 62 +++ src/interfaces/IZkBobDirectDeposits.sol | 30 ++ src/utils/WETHSeller.sol | 50 ++ src/zkbob/ZkBobDirectDepositQueue.sol | 22 +- src/zkbob/ZkBobETHPool.sol | 438 ++++++++++++++++++ test/zkbob/ZkBobETHPool.t.sol | 584 ++++++++++++++++++++++++ 6 files changed, 1185 insertions(+), 1 deletion(-) create mode 100644 src/interfaces/IPermit2.sol create mode 100644 src/utils/WETHSeller.sol create mode 100644 src/zkbob/ZkBobETHPool.sol create mode 100644 test/zkbob/ZkBobETHPool.t.sol diff --git a/src/interfaces/IPermit2.sol b/src/interfaces/IPermit2.sol new file mode 100644 index 0000000..17b2880 --- /dev/null +++ b/src/interfaces/IPermit2.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title Uniswap Permit2 +/// @notice Handles ERC20 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface IPermit2 { + /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount + /// @param maxAmount The maximum amount a spender can request to transfer + error InvalidAmount(uint256 maxAmount); + + /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred + /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred + error LengthMismatch(); + + /// @notice Emits an event when the owner successfully invalidates an unordered nonce. + event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); + + /// @notice Domain separator + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice The token and amount details for a transfer signed in the permit transfer signature + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + /// @dev Recipients and amounts correspond to the index of the signed token permissions array. + /// @dev Reverts if the requested amount is greater than the permitted signed amount. + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Transfers a token using a signed permit message + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) + external; +} diff --git a/src/interfaces/IZkBobDirectDeposits.sol b/src/interfaces/IZkBobDirectDeposits.sol index 51caf36..7002c56 100644 --- a/src/interfaces/IZkBobDirectDeposits.sol +++ b/src/interfaces/IZkBobDirectDeposits.sol @@ -60,6 +60,36 @@ interface IZkBobDirectDeposits { external returns (uint256 depositId); + /** + * @notice Performs a direct deposit to the specified zk address in native token. + * In case the deposit cannot be processed, it can be refunded later to the fallbackReceiver address. + * @param fallbackReceiver receiver of deposit refund. + * @param zkAddress receiver zk address. + * @return depositId id of the submitted deposit to query status for. + */ + function directNativeDeposit( + address fallbackReceiver, + bytes memory zkAddress + ) + external + payable + returns (uint256 depositId); + + /** + * @notice Performs a direct deposit to the specified zk address in native token. + * In case the deposit cannot be processed, it can be refunded later to the fallbackReceiver address. + * @param fallbackReceiver receiver of deposit refund. + * @param zkAddress receiver zk address. + * @return depositId id of the submitted deposit to query status for. + */ + function directNativeDeposit( + address fallbackReceiver, + string memory zkAddress + ) + external + payable + returns (uint256 depositId); + /** * @notice ERC677 callback for performing a direct deposit. * Do not call this function directly, it's only intended to be called by the token contract. diff --git a/src/utils/WETHSeller.sol b/src/utils/WETHSeller.sol new file mode 100644 index 0000000..3433621 --- /dev/null +++ b/src/utils/WETHSeller.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; +import "../interfaces/ITokenSeller.sol"; +import "./Sacrifice.sol"; + +/** + * @title WETHSeller + * Helper for selling WETH for ETH. + */ +contract WETHSeller is ITokenSeller { + IWETH9 immutable weth; + + constructor(address _weth) { + weth = IWETH9(_weth); + } + + /** + * @dev Sells WETH for ETH. + * Prior to calling this function, contract balance of token0 should be greater than or equal to the sold amount. + * Note: this implementation does not include any slippage/sandwich protection, + * users are strongly discouraged from using this contract for exchanging significant amounts. + * @param _receiver native ETH receiver. + * @param _amount amount of tokens to sell. + * @return (received eth amount, refunded token amount). + */ + function sellForETH(address _receiver, uint256 _amount) external returns (uint256, uint256) { + require(weth.balanceOf(address(this)) >= _amount, "WETHSeller: not enough tokens"); + weth.withdraw(_amount); + if (!payable(_receiver).send(_amount)) { + new Sacrifice{value: _amount}(_receiver); + } + return (_amount, 0); + } + + /** + * @dev Estimates amount of received ETH, when selling given amount of tokens via sellForETH function. + * @param _amount amount of tokens to sell. + * @return received eth amount. + */ + function quoteSellForETH(uint256 _amount) external returns (uint256) { + return _amount; + } + + receive() external payable { + require(msg.sender == address(weth), "WETHSeller: not a WETH withdrawal"); + } +} diff --git a/src/zkbob/ZkBobDirectDepositQueue.sol b/src/zkbob/ZkBobDirectDepositQueue.sol index 3583f42..fe3e325 100644 --- a/src/zkbob/ZkBobDirectDepositQueue.sol +++ b/src/zkbob/ZkBobDirectDepositQueue.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.15; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; import "../libraries/ZkAddress.sol"; import "../interfaces/IOperatorManager.sol"; import "../interfaces/IZkBobDirectDeposits.sol"; @@ -182,10 +183,29 @@ contract ZkBobDirectDepositQueue is IZkBobDirectDeposits, IZkBobDirectDepositQue public returns (uint256) { - IERC20(token).safeTransferFrom(msg.sender, address(this), _amount); + IERC20(token).transferFrom(msg.sender, address(this), _amount); return _recordDirectDeposit(msg.sender, _fallbackUser, _amount, _rawZkAddress); } + /// @inheritdoc IZkBobDirectDeposits + function directNativeDeposit( + address _fallbackUser, + string calldata _zkAddress + ) + external + payable + returns (uint256) + { + return directNativeDeposit(_fallbackUser, bytes(_zkAddress)); + } + + /// @inheritdoc IZkBobDirectDeposits + function directNativeDeposit(address _fallbackUser, bytes memory _rawZkAddress) public payable returns (uint256) { + uint256 amount = msg.value; + IWETH9(token).deposit{value: amount}(); + return _recordDirectDeposit(msg.sender, _fallbackUser, amount, _rawZkAddress); + } + /// @inheritdoc IZkBobDirectDeposits function onTokenTransfer(address _from, uint256 _value, bytes calldata _data) external returns (bool) { require(msg.sender == token, "ZkBobDirectDepositQueue: not a token caller"); diff --git a/src/zkbob/ZkBobETHPool.sol b/src/zkbob/ZkBobETHPool.sol new file mode 100644 index 0000000..4b593d1 --- /dev/null +++ b/src/zkbob/ZkBobETHPool.sol @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryImmutableState.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; +import "../interfaces/ITransferVerifier.sol"; +import "../interfaces/ITreeVerifier.sol"; +import "../interfaces/IBatchDepositVerifier.sol"; +import "../interfaces/IMintableERC20.sol"; +import "../interfaces/IOperatorManager.sol"; +import "../interfaces/IERC20Permit.sol"; +import "../interfaces/IPermit2.sol"; +import "../interfaces/ITokenSeller.sol"; +import "../interfaces/IZkBobDirectDepositQueue.sol"; +import "../interfaces/IZkBobPool.sol"; +import "./utils/Parameters.sol"; +import "./utils/ZkBobAccounting.sol"; +import "../utils/Ownable.sol"; +import "../proxy/EIP1967Admin.sol"; +import "../utils/Sacrifice.sol"; + +/** + * @title ZkBobETHPool + * Shielded transactions pool for native and wrappred native tokens. + */ +contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccounting { + using SafeERC20 for IERC20; + + uint256 internal constant MAX_POOL_ID = 0xffffff; + uint256 internal constant TOKEN_DENOMINATOR = 1_000_000_000; + bytes4 internal constant MESSAGE_PREFIX_COMMON_V1 = 0x00000000; + + uint256 public immutable pool_id; + ITransferVerifier public immutable transfer_verifier; + ITreeVerifier public immutable tree_verifier; + IBatchDepositVerifier public immutable batch_deposit_verifier; + IWETH9 public immutable token; + IPermit2 public immutable permit2; + IZkBobDirectDepositQueue public immutable direct_deposit_queue; + + IOperatorManager public operatorManager; + + mapping(uint256 => uint256) public nullifiers; + mapping(uint256 => uint256) public roots; + bytes32 public all_messages_hash; + + mapping(address => uint256) public accumulatedFee; + + ITokenSeller public tokenSeller; + + event UpdateTokenSeller(address seller); + event UpdateOperatorManager(address manager); + event WithdrawFee(address indexed operator, uint256 fee); + + event Message(uint256 indexed index, bytes32 indexed hash, bytes message); + + constructor( + uint256 __pool_id, + address _token, + ITransferVerifier _transfer_verifier, + ITreeVerifier _tree_verifier, + IBatchDepositVerifier _batch_deposit_verifier, + address _direct_deposit_queue, + address _permit2 + ) { + require(__pool_id <= MAX_POOL_ID, "ZkBobPool: exceeds max pool id"); + require(Address.isContract(_token), "ZkBobPool: not a contract"); + require(Address.isContract(address(_transfer_verifier)), "ZkBobPool: not a contract"); + require(Address.isContract(address(_tree_verifier)), "ZkBobPool: not a contract"); + require(Address.isContract(_direct_deposit_queue), "ZkBobPool: not a contract"); + require(Address.isContract(_permit2), "ZkBobPool: not a contract"); + pool_id = __pool_id; + token = IWETH9(_token); + transfer_verifier = _transfer_verifier; + tree_verifier = _tree_verifier; + batch_deposit_verifier = _batch_deposit_verifier; + direct_deposit_queue = IZkBobDirectDepositQueue(_direct_deposit_queue); + permit2 = IPermit2(_permit2); + } + + /** + * @dev Throws if called by any account other than the current relayer operator. + */ + modifier onlyOperator() { + require(operatorManager.isOperator(_msgSender()), "ZkBobPool: not an operator"); + _; + } + + /** + * @dev Initializes pool proxy storage. + * Callable only once and only through EIP1967Proxy constructor / upgradeToAndCall. + * @param _root initial empty merkle tree root. + * @param _tvlCap initial upper cap on the entire pool tvl, 18 decimals. + * @param _dailyDepositCap initial daily limit on the sum of all deposits, 18 decimals. + * @param _dailyWithdrawalCap initial daily limit on the sum of all withdrawals, 18 decimals. + * @param _dailyUserDepositCap initial daily limit on the sum of all per-address deposits, 18 decimals. + * @param _depositCap initial limit on the amount of a single deposit, 18 decimals. + * @param _dailyUserDirectDepositCap initial daily limit on the sum of all per-address direct deposits, 18 decimals. + * @param _directDepositCap initial limit on the amount of a single direct deposit, 18 decimals. + */ + function initialize( + uint256 _root, + uint256 _tvlCap, + uint256 _dailyDepositCap, + uint256 _dailyWithdrawalCap, + uint256 _dailyUserDepositCap, + uint256 _depositCap, + uint256 _dailyUserDirectDepositCap, + uint256 _directDepositCap + ) + external + { + require(msg.sender == address(this), "ZkBobPool: not initializer"); + require(roots[0] == 0, "ZkBobPool: already initialized"); + require(_root != 0, "ZkBobPool: zero root"); + roots[0] = _root; + _setLimits( + 0, + _tvlCap / TOKEN_DENOMINATOR, + _dailyDepositCap / TOKEN_DENOMINATOR, + _dailyWithdrawalCap / TOKEN_DENOMINATOR, + _dailyUserDepositCap / TOKEN_DENOMINATOR, + _depositCap / TOKEN_DENOMINATOR, + _dailyUserDirectDepositCap / TOKEN_DENOMINATOR, + _directDepositCap / TOKEN_DENOMINATOR + ); + } + + /** + * @dev Updates token seller contract used for native coin withdrawals. + * Callable only by the contract owner / proxy admin. + * @param _seller new token seller contract implementation. address(0) will deactivate native withdrawals. + */ + function setTokenSeller(address _seller) external onlyOwner { + tokenSeller = ITokenSeller(_seller); + emit UpdateTokenSeller(_seller); + } + + /** + * @dev Updates used operator manager contract. + * Callable only by the contract owner / proxy admin. + * @param _operatorManager new operator manager implementation. + */ + function setOperatorManager(IOperatorManager _operatorManager) external onlyOwner { + require(address(_operatorManager) != address(0), "ZkBobPool: manager is zero address"); + operatorManager = _operatorManager; + emit UpdateOperatorManager(address(_operatorManager)); + } + + /** + * @dev Tells the denominator for converting BOB into zkBOB units. + * 1e18 BOB units = 1e9 zkBOB units. + */ + function denominator() external pure returns (uint256) { + return TOKEN_DENOMINATOR; + } + + /** + * @dev Tells the current merkle tree index, which will be used for the next operation. + * Each operation increases merkle tree size by 128, so index is equal to the total number of seen operations, multiplied by 128. + * @return next operator merkle index. + */ + function pool_index() external view returns (uint256) { + return _txCount() << 7; + } + + function _root() internal view override returns (uint256) { + return roots[_transfer_index()]; + } + + function _pool_id() internal view override returns (uint256) { + return pool_id; + } + + /** + * @dev Perform a zkBob pool transaction. + * Callable only by the current operator. + * Method uses a custom ABI encoding scheme described in CustomABIDecoder. + * Single transact() call performs either deposit, withdrawal or shielded transfer operation. + */ + function transact() external onlyOperator { + address user; + uint256 txType = _tx_type(); + if (txType == 0) { + user = _deposit_spender(); + } else if (txType == 2) { + user = _memo_receiver(); + } else if (txType == 3) { + user = _memo_permit_holder(); + } + int256 transfer_token_delta = _transfer_token_amount(); + (,, uint256 txCount) = _recordOperation(user, transfer_token_delta); + + uint256 nullifier = _transfer_nullifier(); + { + uint256 _pool_index = txCount << 7; + + require(nullifiers[nullifier] == 0, "ZkBobPool: doublespend detected"); + require(_transfer_index() <= _pool_index, "ZkBobPool: transfer index out of bounds"); + require(transfer_verifier.verifyProof(_transfer_pub(), _transfer_proof()), "ZkBobPool: bad transfer proof"); + require( + tree_verifier.verifyProof(_tree_pub(roots[_pool_index]), _tree_proof()), "ZkBobPool: bad tree proof" + ); + + nullifiers[nullifier] = uint256(keccak256(abi.encodePacked(_transfer_out_commit(), _transfer_delta()))); + _pool_index += 128; + roots[_pool_index] = _tree_root_after(); + bytes memory message = _memo_message(); + // restrict memo message prefix (items count in little endian) to be < 2**16 + require(bytes4(message) & 0x0000ffff == MESSAGE_PREFIX_COMMON_V1, "ZkBobPool: bad message prefix"); + bytes32 message_hash = keccak256(message); + bytes32 _all_messages_hash = keccak256(abi.encodePacked(all_messages_hash, message_hash)); + all_messages_hash = _all_messages_hash; + emit Message(_pool_index, _all_messages_hash, message); + } + + uint256 fee = _memo_fee(); + int256 token_amount = transfer_token_delta + int256(fee); + int256 energy_amount = _transfer_energy_amount(); + + if (txType == 0) { + // Deposit + require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); + token.transferFrom(user, address(this), uint256(token_amount) * TOKEN_DENOMINATOR); + } else if (txType == 1) { + // Transfer + require(token_amount == 0 && energy_amount == 0, "ZkBobPool: incorrect transfer amounts"); + } else if (txType == 2) { + // Withdraw + require(token_amount <= 0 && energy_amount <= 0, "ZkBobPool: incorrect withdraw amounts"); + + uint256 native_amount = _memo_native_amount() * TOKEN_DENOMINATOR; + uint256 withdraw_amount = uint256(-token_amount) * TOKEN_DENOMINATOR; + + if (native_amount > 0) { + ITokenSeller seller = tokenSeller; + if (address(seller) != address(0)) { + token.transfer(address(seller), native_amount); + seller.sellForETH(user, native_amount); + withdraw_amount = withdraw_amount - native_amount; + } + } + + if (withdraw_amount > 0) { + token.transfer(user, withdraw_amount); + } + + // energy withdrawals are not yet implemented, any transaction with non-zero energy_amount will revert + // future version of the protocol will support energy withdrawals through negative energy_amount + if (energy_amount < 0) { + revert("ZkBobPool: XP claiming is not yet enabled"); + } + } else if (txType == 3) { + // Permittable token deposit + require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); + (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); + + bytes memory depositSignature = new bytes(65); + + assembly { + mstore(add(depositSignature, 0x20), r) + mstore(add(depositSignature, 0x40), s) + mstore8(add(depositSignature, 0x60), v) + } + + permit2.permitTransferFrom( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({ + token: address(token), + amount: uint256(token_amount) * TOKEN_DENOMINATOR + }), + nonce: nullifier, + deadline: uint256(_memo_permit_deadline()) + }), + IPermit2.SignatureTransferDetails({ + to: address(this), + requestedAmount: uint256(token_amount) * TOKEN_DENOMINATOR + }), + user, + depositSignature + ); + } else { + revert("ZkBobPool: Incorrect transaction type"); + } + + if (fee > 0) { + accumulatedFee[msg.sender] += fee; + } + } + + /** + * @dev Appends a batch of direct deposits into a zkBob merkle tree. + * Callable only by the current operator. + * @param _root_after new merkle tree root after append. + * @param _indices list of indices for queued pending deposits. + * @param _out_commit out commitment for output notes serialized from direct deposits. + * @param _batch_deposit_proof snark proof for batch deposit verifier. + * @param _tree_proof snark proof for tree update verifier. + */ + function appendDirectDeposits( + uint256 _root_after, + uint256[] calldata _indices, + uint256 _out_commit, + uint256[8] memory _batch_deposit_proof, + uint256[8] memory _tree_proof + ) + external + onlyOperator + { + (uint256 total, uint256 totalFee, uint256 hashsum, bytes memory message) = + direct_deposit_queue.collect(_indices, _out_commit); + + uint256 txCount = _processDirectDepositBatch(total); + uint256 _pool_index = txCount << 7; + + // verify that _out_commit corresponds to zero output account + 16 chosen notes + 111 empty notes + require( + batch_deposit_verifier.verifyProof([hashsum], _batch_deposit_proof), "ZkBobPool: bad batch deposit proof" + ); + + uint256[3] memory tree_pub = [roots[_pool_index], _root_after, _out_commit]; + require(tree_verifier.verifyProof(tree_pub, _tree_proof), "ZkBobPool: bad tree proof"); + + _pool_index += 128; + roots[_pool_index] = _root_after; + bytes32 message_hash = keccak256(message); + bytes32 _all_messages_hash = keccak256(abi.encodePacked(all_messages_hash, message_hash)); + all_messages_hash = _all_messages_hash; + + if (totalFee > 0) { + accumulatedFee[msg.sender] += totalFee; + } + + emit Message(_pool_index, _all_messages_hash, message); + } + + /** + * @dev Records submitted direct deposit into the users limits. + * Callable only by the direct deposit queue. + * @param _sender direct deposit sender. + * @param _amount direct deposit amount in zkBOB units. + */ + function recordDirectDeposit(address _sender, uint256 _amount) external { + require(msg.sender == address(direct_deposit_queue), "ZkBobPool: not authorized"); + _checkDirectDepositLimits(_sender, _amount); + } + + /** + * @dev Withdraws accumulated fee on behalf of an operator. + * Callable only by the operator itself, or by a pre-configured operator fee receiver address. + * @param _operator address of an operator account to withdraw fee from. + * @param _to address of the accumulated fee tokens receiver. + */ + function withdrawFee(address _operator, address _to) external { + require( + _operator == msg.sender || operatorManager.isOperatorFeeReceiver(_operator, msg.sender), + "ZkBobPool: not authorized" + ); + uint256 fee = accumulatedFee[_operator] * TOKEN_DENOMINATOR; + require(fee > 0, "ZkBobPool: no fee to withdraw"); + token.transfer(_to, fee); + accumulatedFee[_operator] = 0; + emit WithdrawFee(_operator, fee); + } + + /** + * @dev Updates pool usage limits. + * Callable only by the contract owner / proxy admin. + * @param _tier pool limits tier (0-254). + * @param _tvlCap new upper cap on the entire pool tvl, 18 decimals. + * @param _dailyDepositCap new daily limit on the sum of all deposits, 18 decimals. + * @param _dailyWithdrawalCap new daily limit on the sum of all withdrawals, 18 decimals. + * @param _dailyUserDepositCap new daily limit on the sum of all per-address deposits, 18 decimals. + * @param _depositCap new limit on the amount of a single deposit, 18 decimals. + * @param _dailyUserDirectDepositCap new daily limit on the sum of all per-address direct deposits, 18 decimals. + * @param _directDepositCap new limit on the amount of a single direct deposit, 18 decimals. + */ + function setLimits( + uint8 _tier, + uint256 _tvlCap, + uint256 _dailyDepositCap, + uint256 _dailyWithdrawalCap, + uint256 _dailyUserDepositCap, + uint256 _depositCap, + uint256 _dailyUserDirectDepositCap, + uint256 _directDepositCap + ) + external + onlyOwner + { + _setLimits( + _tier, + _tvlCap / TOKEN_DENOMINATOR, + _dailyDepositCap / TOKEN_DENOMINATOR, + _dailyWithdrawalCap / TOKEN_DENOMINATOR, + _dailyUserDepositCap / TOKEN_DENOMINATOR, + _depositCap / TOKEN_DENOMINATOR, + _dailyUserDirectDepositCap / TOKEN_DENOMINATOR, + _directDepositCap / TOKEN_DENOMINATOR + ); + } + + /** + * @dev Resets daily limit usage for the current day. + * Callable only by the contract owner / proxy admin. + * @param _tier tier id to reset daily limits for. + */ + function resetDailyLimits(uint8 _tier) external onlyOwner { + _resetDailyLimits(_tier); + } + + /** + * @dev Updates users limit tiers. + * Callable only by the contract owner / proxy admin. + * @param _tier pool limits tier (0-255). + * 0 is the default tier. + * 1-254 are custom pool limit tiers, configured at runtime. + * 255 is the special tier with zero limits, used to effectively prevent some address from accessing the pool. + * @param _users list of user account addresses to assign a tier for. + */ + function setUsersTier(uint8 _tier, address[] memory _users) external onlyOwner { + _setUsersTier(_tier, _users); + } + + /** + * @dev Tells if caller is the contract owner. + * Gives ownership rights to the proxy admin as well. + * @return true, if caller is the contract owner or proxy admin. + */ + function _isOwner() internal view override returns (bool) { + return super._isOwner() || _admin() == _msgSender(); + } +} diff --git a/test/zkbob/ZkBobETHPool.t.sol b/test/zkbob/ZkBobETHPool.t.sol new file mode 100644 index 0000000..a2767bc --- /dev/null +++ b/test/zkbob/ZkBobETHPool.t.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import "../shared/Env.t.sol"; +import "../mocks/TransferVerifierMock.sol"; +import "../mocks/TreeUpdateVerifierMock.sol"; +import "../mocks/BatchDepositVerifierMock.sol"; +import "../mocks/DummyImpl.sol"; +import "../../src/proxy/EIP1967Proxy.sol"; +import "../../src/zkbob/ZkBobETHPool.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; +import "../../src/BobToken.sol"; +import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../shared/ForkTests.t.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; +import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; +import "../../src/utils/WETHSeller.sol"; + +contract ZkBobETHPoolTest is AbstractMainnetForkTest { + uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; + + address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; + address constant uniV3Positions = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address constant usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant permit2Address = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + bytes constant zkAddress = "QsnTijXekjRm9hKcq5kLNPsa6P4HtMRrc3RxVx3jsLHeo2AiysYxVJP86mriHfN"; + + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + ZkBobETHPool pool; + ZkBobDirectDepositQueue queue; + IWETH9 token; + IOperatorManager operatorManager; + IPermit2 permit2; + + event Message(uint256 indexed index, bytes32 indexed hash, bytes message); + + event SubmitDirectDeposit( + address indexed sender, + uint256 indexed nonce, + address fallbackUser, + ZkAddress.ZkAddress zkAddress, + uint64 deposit + ); + event RefundDirectDeposit(uint256 indexed nonce, address receiver, uint256 amount); + event CompleteDirectDepositBatch(uint256[] indices); + + function setUp() public { + vm.createSelectFork(forkRpcUrl, forkBlock); + token = IWETH9(weth); + permit2 = IPermit2(permit2Address); + + EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); + EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); + + console2.log(weth, address(token)); + ZkBobETHPool impl = + new ZkBobETHPool(0, address(token), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy), permit2Address); + + bytes memory initData = abi.encodeWithSelector( + ZkBobETHPool.initialize.selector, + initialRoot, + 1_000_000 ether, + 100_000 ether, + 100_000 ether, + 10_000 ether, + 10_000 ether, + 0, + 0 + ); + poolProxy.upgradeToAndCall(address(impl), initData); + pool = ZkBobETHPool(address(poolProxy)); + + ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(token)); + queueProxy.upgradeTo(address(queueImpl)); + queue = ZkBobDirectDepositQueue(address(queueProxy)); + + operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); + pool.setOperatorManager(operatorManager); + + WETHSeller wethSeller = new WETHSeller(weth); + pool.setTokenSeller(address(wethSeller)); + queue.setOperatorManager(operatorManager); + queue.setDirectDepositFee(0.1 gwei); + queue.setDirectDepositTimeout(1 days); + + deal(weth, user1, 1 ether); + vm.startPrank(user1); + token.approve(permit2Address, type(uint256).max); + token.approve(address(queue), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(user2); + token.approve(permit2Address, type(uint256).max); + token.approve(address(queue), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(user3); + token.approve(permit2Address, type(uint256).max); + token.approve(address(queue), type(uint256).max); + vm.stopPrank(); + } + + function testSimpleTransaction() public { + bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodeTransfer(); + _transact(data2); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(token.balanceOf(user3), 0.02 ether); + } + + function testGetters() public { + assertEq(pool.pool_index(), 0); + assertEq(pool.denominator(), 1 gwei); + + bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data1); + + assertEq(pool.pool_index(), 128); + + bytes memory data2 = _encodeTransfer(); + _transact(data2); + + assertEq(pool.pool_index(), 256); + } + + function testAuthRights() public { + vm.startPrank(user1); + + vm.expectRevert("ZkBobPool: not initializer"); + pool.initialize(0, 0, 0, 0, 0, 0, 0, 0); + vm.expectRevert("Ownable: caller is not the owner"); + pool.setOperatorManager(IOperatorManager(address(0))); + vm.expectRevert("Ownable: caller is not the owner"); + pool.setTokenSeller(address(0)); + vm.expectRevert("Ownable: caller is not the owner"); + pool.setLimits(0, 0, 0, 0, 0, 0, 0, 0); + vm.expectRevert("Ownable: caller is not the owner"); + pool.setUsersTier(0, new address[](1)); + vm.expectRevert("Ownable: caller is not the owner"); + pool.resetDailyLimits(0); + + vm.stopPrank(); + } + + function testUsersTiers() public { + pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 0, 0); + address[] memory users = new address[](1); + users[0] = user2; + pool.setUsersTier(1, users); + + assertEq(pool.getLimitsFor(user1).tier, 0); + assertEq(pool.getLimitsFor(user1).depositCap, 10_000 gwei); + assertEq(pool.getLimitsFor(user2).tier, 1); + assertEq(pool.getLimitsFor(user2).depositCap, 20_000 gwei); + } + + function testResetDailyLimits() public { + deal(weth, user1, 10 ether); + + bytes memory data1 = _encodePermitDeposit(5 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodeWithdrawal(user1, 4 ether, 0 ether); + _transact(data2); + + assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 5 gwei); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 gwei); + + pool.resetDailyLimits(0); + + assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 0); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 0); + } + + function testSetOperatorManager() public { + assertEq(address(pool.operatorManager()), address(operatorManager)); + + IOperatorManager newOperatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); + pool.setOperatorManager(newOperatorManager); + + assertEq(address(pool.operatorManager()), address(newOperatorManager)); + } + + function testPermitDeposit() public { + bytes memory data = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(token.balanceOf(user1), 0.49 ether); + assertEq(token.balanceOf(address(pool)), 0.5 ether); + assertEq(token.balanceOf(user3), 0.01 ether); + } + + function testUsualDeposit() public { + vm.prank(user1); + token.approve(address(pool), 0.51 ether); + + bytes memory data = _encodeDeposit(0.5 ether, 0.01 ether); + _transact(data); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(token.balanceOf(user1), 0.49 ether); + assertEq(token.balanceOf(address(pool)), 0.5 ether); + assertEq(token.balanceOf(user3), 0.01 ether); + } + + function testWithdrawal() public { + bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether, 0 ether); + _transact(data2); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(token.balanceOf(user1), 0.59 ether); + assertEq(token.balanceOf(address(pool)), 0.39 ether); + assertEq(token.balanceOf(user3), 0.02 ether); + } + + function testNativeWithdrawal() public { + vm.deal(user1, 0); + + bytes memory data1 = _encodePermitDeposit(0.99 ether, 0.01 ether); + _transact(data1); + + // user1 withdraws 0.4 BOB, 0.3 BOB gets converted to ETH + uint256 quote2 = 0.3 ether; + bytes memory data2 = _encodeWithdrawal(user1, 0.4 ether, 0.3 ether); + _transact(data2); + + address dummy = address(new DummyImpl(0)); + uint256 quote3 = 0.3 ether; + bytes memory data3 = _encodeWithdrawal(dummy, 0.4 ether, 0.3 ether); + _transact(data3); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(token.balanceOf(user1), 0.1 ether); + assertEq(token.balanceOf(dummy), 0.1 ether); + assertEq(token.balanceOf(address(pool)), 0.17 ether); + assertEq(token.balanceOf(user3), 0.03 ether); + assertGt(user1.balance, 1 gwei); + assertEq(user1.balance, quote2); + assertGt(dummy.balance, 1 gwei); + assertEq(dummy.balance, quote3); + } + + function testRejectNegativeDeposits() public { + bytes memory data1 = _encodePermitDeposit(0.99 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodePermitDeposit(-0.5 ether, 1 ether); + _transactReverted(data2, "ZkBobPool: incorrect deposit amounts"); + + vm.prank(user1); + token.approve(address(pool), 0.5 ether); + + bytes memory data3 = _encodeDeposit(-0.5 ether, 1 ether); + _transactReverted(data3, "ZkBobPool: incorrect deposit amounts"); + } + + function _setUpDD() internal { + deal(user1, 100 ether); + deal(address(token), user1, 100 ether); + deal(address(token), user2, 100 ether); + + pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 25 ether, 10 ether); + address[] memory users = new address[](1); + users[0] = user1; + pool.setUsersTier(1, users); + + queue.setDirectDepositFee(0.1 gwei); + } + + function testDirectDepositSubmit() public { + _setUpDD(); + + vm.prank(user2); + vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); + queue.directDeposit(user2, 10 ether, zkAddress); + + vm.startPrank(user1); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit amount is too low"); + queue.directDeposit(user2, 0.01 ether, zkAddress); + + vm.expectRevert(ZkAddress.InvalidZkAddressLength.selector); + queue.directDeposit(user2, 10 ether, bytes("invalid")); + + vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); + queue.directDeposit(user2, 15 ether, zkAddress); + + vm.expectEmit(true, true, false, true); + emit SubmitDirectDeposit(user1, 0, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); + queue.directDeposit(user2, 10 ether, zkAddress); + + vm.expectEmit(true, true, false, true); + emit SubmitDirectDeposit(user1, 1, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); + queue.directDeposit(user2, 10 ether, zkAddress); + + vm.expectRevert("ZkBobAccounting: daily user direct deposit cap exceeded"); + queue.directDeposit(user2, 10 ether, zkAddress); + + for (uint256 i = 0; i < 2; i++) { + IZkBobDirectDeposits.DirectDeposit memory deposit = queue.getDirectDeposit(i); + assertEq(deposit.fallbackReceiver, user2); + assertEq(deposit.sent, 10 ether); + assertEq(deposit.deposit, 9.9 gwei); + assertEq(deposit.fee, 0.1 gwei); + assertEq(uint8(deposit.status), uint8(IZkBobDirectDeposits.DirectDepositStatus.Pending)); + } + vm.stopPrank(); + } + + function testAppendDirectDeposits() public { + _setUpDD(); + + vm.prank(user1); + queue.directNativeDeposit{value: 10 ether}(user2, zkAddress); + + vm.prank(user1); + queue.directNativeDeposit{value: 5 ether}(user2, zkAddress); + + uint256[] memory indices = new uint256[](2); + indices[0] = 0; + indices[1] = 1; + address verifier = address(pool.batch_deposit_verifier()); + uint256 outCommitment = _randFR(); + bytes memory data = abi.encodePacked( + outCommitment, + bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(9.9 gwei), // first deposit amount + bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(4.9 gwei), // second deposit amount + new bytes(14 * 50) + ); + vm.expectCall( + verifier, + abi.encodeWithSelector( + IBatchDepositVerifier.verifyProof.selector, + [ + uint256(keccak256(data)) % 21888242871839275222246405745257275088548364400416034343698204186575808495617 + ] + ) + ); + vm.expectEmit(true, false, false, true); + emit CompleteDirectDepositBatch(indices); + bytes memory message = abi.encodePacked( + bytes4(0x02000001), // uint16(2) in little endian ++ MESSAGE_PREFIX_DIRECT_DEPOSIT_V1 + uint64(0), // first deposit nonce + bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(9.9 gwei), // first deposit amount + uint64(1), // second deposit nonce + bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(4.9 gwei) // second deposit amount + ); + vm.expectEmit(true, false, false, true); + emit Message(128, bytes32(0), message); + vm.prank(user2); + pool.appendDirectDeposits(_randFR(), indices, outCommitment, _randProof(), _randProof()); + } + + function testRefundDirectDeposit() public { + _setUpDD(); + + vm.prank(user1); + queue.directNativeDeposit{value: 10 ether + 1}(user2, zkAddress); + + vm.prank(user1); + queue.directNativeDeposit{value: 5 ether + 1}(user2, zkAddress); + + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); + queue.refundDirectDeposit(0); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); + queue.refundDirectDeposit(1); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); + queue.refundDirectDeposit(2); + + deal(address(token), user2, 0); + + vm.prank(user2); + vm.expectEmit(true, false, false, true); + emit RefundDirectDeposit(0, user2, 10 ether + 1); + queue.refundDirectDeposit(0); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); + queue.refundDirectDeposit(0); + assertEq(token.balanceOf(user2), 10 ether + 1); + + skip(2 days); + + vm.expectEmit(true, false, false, true); + emit RefundDirectDeposit(1, user2, 5 ether + 1); + queue.refundDirectDeposit(1); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); + queue.refundDirectDeposit(1); + assertEq(token.balanceOf(user2), 15 ether + 2); + } + + function testDepositForUserWithKYCPassed() public { + uint8 tier = 254; + ERC721PresetMinterPauserAutoId nft = new ERC721PresetMinterPauserAutoId("Test NFT", "tNFT", "http://nft.url/"); + + SimpleKYCProviderManager manager = new SimpleKYCProviderManager(nft, tier); + pool.setKycProvidersManager(manager); + + pool.setLimits(tier, 50 ether, 10 ether, 2 ether, 6 ether, 5 ether, 0, 0); + address[] memory users = new address[](1); + users[0] = user1; + pool.setUsersTier(tier, users); + + nft.mint(user1); + + deal(weth, address(user1), 10 ether); + + bytes memory data = _encodePermitDeposit(4 ether, 0.01 ether); + _transact(data); + + bytes memory data2 = _encodeWithdrawal(user1, 1 ether, 0 ether); + _transact(data2); + + bytes memory data3 = _encodePermitDeposit(3 ether, 0.01 ether); + _transactReverted(data3, "ZkBobAccounting: daily user deposit cap exceeded"); + + bytes memory data4 = _encodeWithdrawal(user1, 2 ether, 0 ether); + _transactReverted(data4, "ZkBobAccounting: daily withdrawal cap exceeded"); + + assertEq(pool.getLimitsFor(user1).dailyUserDepositCapUsage, 4 gwei); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 1.01 gwei); // 1 requested + 0.01 fees + } + + function _encodePermitDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { + uint256 expiry = block.timestamp + 1 hours; + bytes32 nullifier = bytes32(_randFR()); + (uint8 v, bytes32 r, bytes32 s) = + _signSaltedPermit(pk1, user1, address(pool), uint256(_amount + int256(_fee)), expiry, nullifier); + bytes memory data = abi.encodePacked( + ZkBobETHPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + data = abi.encodePacked( + data, uint16(3), uint16(72), uint64(_fee / 1 gwei), uint64(expiry), user1, bytes4(0x01000000), _randFR() + ); + return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); + } + + function _encodeDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { + bytes32 nullifier = bytes32(_randFR()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, ECDSA.toEthSignedMessageHash(nullifier)); + bytes memory data = abi.encodePacked( + ZkBobETHPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + data = abi.encodePacked(data, uint16(0), uint16(44), uint64(_fee / 1 gwei), bytes4(0x01000000), _randFR()); + return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); + } + + function _encodeWithdrawal(address _to, uint256 _amount, uint256 _nativeAmount) internal returns (bytes memory) { + bytes memory data = abi.encodePacked( + ZkBobETHPool.transact.selector, + _randFR(), + _randFR(), + uint48(0), + uint112(0), + int64(-int256((_amount + 0.01 ether) / 1 gwei)) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + return abi.encodePacked( + data, + uint16(2), + uint16(72), + uint64(0.01 ether / 1 gwei), + uint64(_nativeAmount / 1 gwei), + _to, + bytes4(0x01000000), + _randFR() + ); + } + + function _encodeTransfer() internal returns (bytes memory) { + bytes memory data = abi.encodePacked( + ZkBobETHPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), int64(-0.01 ether / 1 gwei) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + return abi.encodePacked(data, uint16(1), uint16(44), uint64(0.01 ether / 1 gwei), bytes4(0x01000000), _randFR()); + } + + function _transact(bytes memory _data) internal { + vm.prank(user2); + (bool status,) = address(pool).call(_data); + require(status, "transact() reverted"); + } + + function _transactReverted(bytes memory _data, bytes memory _revertReason) internal { + vm.prank(user2); + (bool status, bytes memory returnData) = address(pool).call(_data); + assert(!status); + assertEq(returnData, abi.encodeWithSignature("Error(string)", _revertReason)); + } + + function _getEIP712Hash( + IPermit2.PermitTransferFrom memory permit, + address spender + ) + internal + view + returns (bytes32 h) + { + return keccak256( + abi.encodePacked( + "\x19\x01", + permit2.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + PERMIT_TRANSFER_FROM_TYPEHASH, + keccak256( + abi.encode(TOKEN_PERMISSIONS_TYPEHASH, permit.permitted.token, permit.permitted.amount) + ), + spender, + permit.nonce, + permit.deadline + ) + ) + ) + ); + } + + function _signSaltedPermit( + uint256 _pk, + address _holder, + address _spender, + uint256 _value, + uint256 _expiry, + bytes32 _salt + ) + internal + returns (uint8 v, bytes32 r, bytes32 s) + { + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: weth, amount: _value}), + nonce: uint256(_salt), + deadline: _expiry + }); + return vm.sign(_pk, _getEIP712Hash(permit, _spender)); + } + + function _randFR() private returns (uint256) { + return uint256(keccak256(abi.encode(gasleft()))) + % 21888242871839275222246405745257275088696311157297823662689037894645226208583; + } + + function _randProof() private returns (uint256[8] memory) { + return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; + } +} From ad77d5f0e5dc3f2f1c14e8b4054439972823d4fa Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Thu, 30 Mar 2023 12:10:44 +0300 Subject: [PATCH 02/11] rollback direct deposit queue --- src/zkbob/ZkBobDirectDepositQueue.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zkbob/ZkBobDirectDepositQueue.sol b/src/zkbob/ZkBobDirectDepositQueue.sol index fe3e325..d7a5d99 100644 --- a/src/zkbob/ZkBobDirectDepositQueue.sol +++ b/src/zkbob/ZkBobDirectDepositQueue.sol @@ -183,7 +183,7 @@ contract ZkBobDirectDepositQueue is IZkBobDirectDeposits, IZkBobDirectDepositQue public returns (uint256) { - IERC20(token).transferFrom(msg.sender, address(this), _amount); + IERC20(token).safeTransferFrom(msg.sender, address(this), _amount); return _recordDirectDeposit(msg.sender, _fallbackUser, _amount, _rawZkAddress); } From 2c73d0b6b8e13d5a0e7e90bc59c8cbd5dd4be3f7 Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Thu, 30 Mar 2023 12:20:32 +0300 Subject: [PATCH 03/11] Update --- src/utils/WETHSeller.sol | 50 ----------------------------------- src/zkbob/ZkBobETHPool.sol | 33 ++++++++--------------- test/zkbob/ZkBobETHPool.t.sol | 7 +---- 3 files changed, 12 insertions(+), 78 deletions(-) delete mode 100644 src/utils/WETHSeller.sol diff --git a/src/utils/WETHSeller.sol b/src/utils/WETHSeller.sol deleted file mode 100644 index 3433621..0000000 --- a/src/utils/WETHSeller.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.15; - -import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; -import "../interfaces/ITokenSeller.sol"; -import "./Sacrifice.sol"; - -/** - * @title WETHSeller - * Helper for selling WETH for ETH. - */ -contract WETHSeller is ITokenSeller { - IWETH9 immutable weth; - - constructor(address _weth) { - weth = IWETH9(_weth); - } - - /** - * @dev Sells WETH for ETH. - * Prior to calling this function, contract balance of token0 should be greater than or equal to the sold amount. - * Note: this implementation does not include any slippage/sandwich protection, - * users are strongly discouraged from using this contract for exchanging significant amounts. - * @param _receiver native ETH receiver. - * @param _amount amount of tokens to sell. - * @return (received eth amount, refunded token amount). - */ - function sellForETH(address _receiver, uint256 _amount) external returns (uint256, uint256) { - require(weth.balanceOf(address(this)) >= _amount, "WETHSeller: not enough tokens"); - weth.withdraw(_amount); - if (!payable(_receiver).send(_amount)) { - new Sacrifice{value: _amount}(_receiver); - } - return (_amount, 0); - } - - /** - * @dev Estimates amount of received ETH, when selling given amount of tokens via sellForETH function. - * @param _amount amount of tokens to sell. - * @return received eth amount. - */ - function quoteSellForETH(uint256 _amount) external returns (uint256) { - return _amount; - } - - receive() external payable { - require(msg.sender == address(weth), "WETHSeller: not a WETH withdrawal"); - } -} diff --git a/src/zkbob/ZkBobETHPool.sol b/src/zkbob/ZkBobETHPool.sol index 4b593d1..645fd68 100644 --- a/src/zkbob/ZkBobETHPool.sol +++ b/src/zkbob/ZkBobETHPool.sol @@ -15,7 +15,6 @@ import "../interfaces/IMintableERC20.sol"; import "../interfaces/IOperatorManager.sol"; import "../interfaces/IERC20Permit.sol"; import "../interfaces/IPermit2.sol"; -import "../interfaces/ITokenSeller.sol"; import "../interfaces/IZkBobDirectDepositQueue.sol"; import "../interfaces/IZkBobPool.sol"; import "./utils/Parameters.sol"; @@ -51,9 +50,6 @@ contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAcc mapping(address => uint256) public accumulatedFee; - ITokenSeller public tokenSeller; - - event UpdateTokenSeller(address seller); event UpdateOperatorManager(address manager); event WithdrawFee(address indexed operator, uint256 fee); @@ -131,16 +127,6 @@ contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAcc ); } - /** - * @dev Updates token seller contract used for native coin withdrawals. - * Callable only by the contract owner / proxy admin. - * @param _seller new token seller contract implementation. address(0) will deactivate native withdrawals. - */ - function setTokenSeller(address _seller) external onlyOwner { - tokenSeller = ITokenSeller(_seller); - emit UpdateTokenSeller(_seller); - } - /** * @dev Updates used operator manager contract. * Callable only by the contract owner / proxy admin. @@ -226,7 +212,7 @@ contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAcc if (txType == 0) { // Deposit require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - token.transferFrom(user, address(this), uint256(token_amount) * TOKEN_DENOMINATOR); + IERC20(token).safeTransferFrom(user, address(this), uint256(token_amount) * TOKEN_DENOMINATOR); } else if (txType == 1) { // Transfer require(token_amount == 0 && energy_amount == 0, "ZkBobPool: incorrect transfer amounts"); @@ -238,16 +224,15 @@ contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAcc uint256 withdraw_amount = uint256(-token_amount) * TOKEN_DENOMINATOR; if (native_amount > 0) { - ITokenSeller seller = tokenSeller; - if (address(seller) != address(0)) { - token.transfer(address(seller), native_amount); - seller.sellForETH(user, native_amount); - withdraw_amount = withdraw_amount - native_amount; + token.withdraw(native_amount); + if (!payable(user).send(native_amount)) { + new Sacrifice{value: native_amount}(user); } + withdraw_amount = withdraw_amount - native_amount; } if (withdraw_amount > 0) { - token.transfer(user, withdraw_amount); + IERC20(token).safeTransfer(user, withdraw_amount); } // energy withdrawals are not yet implemented, any transaction with non-zero energy_amount will revert @@ -363,7 +348,7 @@ contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAcc ); uint256 fee = accumulatedFee[_operator] * TOKEN_DENOMINATOR; require(fee > 0, "ZkBobPool: no fee to withdraw"); - token.transfer(_to, fee); + IERC20(token).safeTransfer(_to, fee); accumulatedFee[_operator] = 0; emit WithdrawFee(_operator, fee); } @@ -435,4 +420,8 @@ contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAcc function _isOwner() internal view override returns (bool) { return super._isOwner() || _admin() == _msgSender(); } + + receive() external payable { + require(msg.sender == address(token), "Not a WETH withdrawal"); + } } diff --git a/test/zkbob/ZkBobETHPool.t.sol b/test/zkbob/ZkBobETHPool.t.sol index a2767bc..3c6dd14 100644 --- a/test/zkbob/ZkBobETHPool.t.sol +++ b/test/zkbob/ZkBobETHPool.t.sol @@ -19,7 +19,6 @@ import "../../src/zkbob/manager/MutableOperatorManager.sol"; import "../shared/ForkTests.t.sol"; import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; -import "../../src/utils/WETHSeller.sol"; contract ZkBobETHPoolTest is AbstractMainnetForkTest { uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; @@ -80,7 +79,7 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { 0 ); poolProxy.upgradeToAndCall(address(impl), initData); - pool = ZkBobETHPool(address(poolProxy)); + pool = ZkBobETHPool(payable(address(poolProxy))); ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(token)); queueProxy.upgradeTo(address(queueImpl)); @@ -89,8 +88,6 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); pool.setOperatorManager(operatorManager); - WETHSeller wethSeller = new WETHSeller(weth); - pool.setTokenSeller(address(wethSeller)); queue.setOperatorManager(operatorManager); queue.setDirectDepositFee(0.1 gwei); queue.setDirectDepositTimeout(1 days); @@ -147,8 +144,6 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { vm.expectRevert("Ownable: caller is not the owner"); pool.setOperatorManager(IOperatorManager(address(0))); vm.expectRevert("Ownable: caller is not the owner"); - pool.setTokenSeller(address(0)); - vm.expectRevert("Ownable: caller is not the owner"); pool.setLimits(0, 0, 0, 0, 0, 0, 0, 0); vm.expectRevert("Ownable: caller is not the owner"); pool.setUsersTier(0, new address[](1)); From cc13929f42fb54b0a3b4ef08c7045e02285894aa Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Fri, 31 Mar 2023 16:48:37 +0300 Subject: [PATCH 04/11] Refactoring & deploy scripts --- script/scripts/Env.s.sol | 4 + script/scripts/Local.s.sol | 5 +- ...Impl.s.sol => NewZkBobPoolERC20Impl.s.sol} | 9 +- script/scripts/NewZkBobPoolETHImpl.s.sol | 32 ++ .../{ZkBobPool.s.sol => ZkBobPoolERC20.s.sol} | 7 +- script/scripts/ZkBobPoolETH.s.sol | 98 ++++ src/interfaces/IZkBobDirectDeposits.sol | 30 -- src/interfaces/IZkBobDirectDepositsETH.sol | 37 ++ src/zkbob/ZkBobDirectDepositQueue.sol | 19 - src/zkbob/ZkBobDirectDepositQueueETH.sol | 43 ++ src/zkbob/ZkBobETHPool.sol | 427 ------------------ src/zkbob/ZkBobPool.sol | 55 ++- src/zkbob/ZkBobPoolERC20.sol | 75 +++ src/zkbob/ZkBobPoolETH.sol | 205 +++++++++ .../{ZkBobPool.t.sol => ZkBobPoolERC20.t.sol} | 11 +- ...{ZkBobETHPool.t.sol => ZkBobPoolETH.t.sol} | 29 +- 16 files changed, 558 insertions(+), 528 deletions(-) rename script/scripts/{NewZkBobPoolImpl.s.sol => NewZkBobPoolERC20Impl.s.sol} (68%) create mode 100644 script/scripts/NewZkBobPoolETHImpl.s.sol rename script/scripts/{ZkBobPool.s.sol => ZkBobPoolERC20.s.sol} (95%) create mode 100644 script/scripts/ZkBobPoolETH.s.sol create mode 100644 src/interfaces/IZkBobDirectDepositsETH.sol create mode 100644 src/zkbob/ZkBobDirectDepositQueueETH.sol delete mode 100644 src/zkbob/ZkBobETHPool.sol create mode 100644 src/zkbob/ZkBobPoolERC20.sol create mode 100644 src/zkbob/ZkBobPoolETH.sol rename test/zkbob/{ZkBobPool.t.sol => ZkBobPoolERC20.t.sol} (98%) rename test/zkbob/{ZkBobETHPool.t.sol => ZkBobPoolETH.t.sol} (95%) diff --git a/script/scripts/Env.s.sol b/script/scripts/Env.s.sol index 5a28802..ee65da9 100644 --- a/script/scripts/Env.s.sol +++ b/script/scripts/Env.s.sol @@ -52,3 +52,7 @@ address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; uint24 constant fee0 = 500; address constant usdc = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; uint24 constant fee1 = 500; + +// zkbobpool eth +address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +address constant permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; diff --git a/script/scripts/Local.s.sol b/script/scripts/Local.s.sol index 1748d5c..c2adda4 100644 --- a/script/scripts/Local.s.sol +++ b/script/scripts/Local.s.sol @@ -8,6 +8,7 @@ import "../../test/shared/EIP2470.t.sol"; import "../../src/BobToken.sol"; import "../../src/proxy/EIP1967Proxy.sol"; import "../../src/zkbob/ZkBobPool.sol"; +import "../../src/zkbob/ZkBobPoolERC20.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; @@ -43,7 +44,7 @@ contract DeployLocal is Script { EIP1967Proxy poolProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); EIP1967Proxy queueProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); - ZkBobPool poolImpl = new ZkBobPool( + ZkBobPoolERC20 poolImpl = new ZkBobPoolERC20( zkBobPoolId, address(bob), transferVerifier, @@ -65,7 +66,7 @@ contract DeployLocal is Script { ); poolProxy.upgradeToAndCall(address(poolImpl), initData); } - ZkBobPool pool = ZkBobPool(address(poolProxy)); + ZkBobPoolERC20 pool = ZkBobPoolERC20(address(poolProxy)); ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(bob)); queueProxy.upgradeTo(address(queueImpl)); diff --git a/script/scripts/NewZkBobPoolImpl.s.sol b/script/scripts/NewZkBobPoolERC20Impl.s.sol similarity index 68% rename from script/scripts/NewZkBobPoolImpl.s.sol rename to script/scripts/NewZkBobPoolERC20Impl.s.sol index f233bc6..6e8bd88 100644 --- a/script/scripts/NewZkBobPoolImpl.s.sol +++ b/script/scripts/NewZkBobPoolERC20Impl.s.sol @@ -7,14 +7,15 @@ import "./Env.s.sol"; import "../../src/proxy/EIP1967Proxy.sol"; import "../../src/zkbob/ZkBobPool.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../../src/zkbob/ZkBobPoolERC20.sol"; -contract DeployNewZkBobPoolImpl is Script { +contract DeployNewZkBobPoolERC20Impl is Script { function run() external { vm.startBroadcast(); - ZkBobPool pool = ZkBobPool(zkBobPool); + ZkBobPoolERC20 pool = ZkBobPoolERC20(zkBobPool); - ZkBobPool impl = new ZkBobPool( + ZkBobPoolERC20 impl = new ZkBobPoolERC20( pool.pool_id(), pool.token(), pool.transfer_verifier(), @@ -25,6 +26,6 @@ contract DeployNewZkBobPoolImpl is Script { vm.stopBroadcast(); - console2.log("ZkBobPool implementation:", address(impl)); + console2.log("ZkBobPoolERC20 implementation:", address(impl)); } } diff --git a/script/scripts/NewZkBobPoolETHImpl.s.sol b/script/scripts/NewZkBobPoolETHImpl.s.sol new file mode 100644 index 0000000..619ee1b --- /dev/null +++ b/script/scripts/NewZkBobPoolETHImpl.s.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Script.sol"; +import "./Env.s.sol"; +import "../../src/proxy/EIP1967Proxy.sol"; +import "../../src/zkbob/ZkBobPool.sol"; +import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../../src/zkbob/ZkBobPoolETH.sol"; + +contract DeployNewZkBobPoolETHImpl is Script { + function run() external { + vm.startBroadcast(); + + ZkBobPoolETH pool = ZkBobPoolETH(zkBobPool); + + ZkBobPoolETH impl = new ZkBobPoolETH( + pool.pool_id(), + pool.token(), + pool.transfer_verifier(), + pool.tree_verifier(), + pool.batch_deposit_verifier(), + address(pool.direct_deposit_queue()), + address(pool.permit2) + ); + + vm.stopBroadcast(); + + console2.log("ZkBobPoolETH implementation:", address(impl)); + } +} diff --git a/script/scripts/ZkBobPool.s.sol b/script/scripts/ZkBobPoolERC20.s.sol similarity index 95% rename from script/scripts/ZkBobPool.s.sol rename to script/scripts/ZkBobPoolERC20.s.sol index 7b6d2a0..90ecb38 100644 --- a/script/scripts/ZkBobPool.s.sol +++ b/script/scripts/ZkBobPoolERC20.s.sol @@ -8,8 +8,9 @@ import "../../src/proxy/EIP1967Proxy.sol"; import "../../src/zkbob/ZkBobPool.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../../src/zkbob/ZkBobPoolERC20.sol"; -contract DeployZkBobPool is Script { +contract DeployZkBobPoolERC20 is Script { function run() external { vm.startBroadcast(); @@ -32,7 +33,7 @@ contract DeployZkBobPool is Script { EIP1967Proxy poolProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); EIP1967Proxy queueProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); - ZkBobPool poolImpl = new ZkBobPool( + ZkBobPoolERC20 poolImpl = new ZkBobPoolERC20( zkBobPoolId, bobVanityAddr, transferVerifier, @@ -52,7 +53,7 @@ contract DeployZkBobPool is Script { zkBobDirectDepositCap ); poolProxy.upgradeToAndCall(address(poolImpl), initData); - ZkBobPool pool = ZkBobPool(address(poolProxy)); + ZkBobPoolERC20 pool = ZkBobPoolERC20(address(poolProxy)); ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), bobVanityAddr); queueProxy.upgradeTo(address(queueImpl)); diff --git a/script/scripts/ZkBobPoolETH.s.sol b/script/scripts/ZkBobPoolETH.s.sol new file mode 100644 index 0000000..6ef3383 --- /dev/null +++ b/script/scripts/ZkBobPoolETH.s.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Script.sol"; +import "./Env.s.sol"; +import "../../src/proxy/EIP1967Proxy.sol"; +import "../../src/zkbob/ZkBobPool.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; +import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../../src/zkbob/ZkBobPoolETH.sol"; + +contract DeployZkBobPoolETH is Script { + function run() external { + vm.startBroadcast(); + + ITransferVerifier transferVerifier; + ITreeVerifier treeVerifier; + IBatchDepositVerifier batchDepositVerifier; + bytes memory code1 = + vm.getCode(string.concat("out/", zkBobVerifiers, "/TransferVerifier.sol/TransferVerifier.json")); + bytes memory code2 = + vm.getCode(string.concat("out/", zkBobVerifiers, "/TreeUpdateVerifier.sol/TreeUpdateVerifier.json")); + bytes memory code3 = vm.getCode( + string.concat("out/", zkBobVerifiers, "/DelegatedDepositVerifier.sol/DelegatedDepositVerifier.json") + ); + assembly { + transferVerifier := create(0, add(code1, 0x20), mload(code1)) + treeVerifier := create(0, add(code2, 0x20), mload(code2)) + batchDepositVerifier := create(0, add(code3, 0x20), mload(code3)) + } + + EIP1967Proxy poolProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); + EIP1967Proxy queueProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); + + ZkBobPoolETH poolImpl = new ZkBobPoolETH( + zkBobPoolId, + weth, + transferVerifier, + treeVerifier, + batchDepositVerifier, + address(queueProxy), + permit2 + ); + bytes memory initData = abi.encodeWithSelector( + ZkBobPool.initialize.selector, + zkBobInitialRoot, + zkBobPoolCap, + zkBobDailyDepositCap, + zkBobDailyWithdrawalCap, + zkBobDailyUserDepositCap, + zkBobDepositCap, + zkBobDailyUserDirectDepositCap, + zkBobDirectDepositCap + ); + poolProxy.upgradeToAndCall(address(poolImpl), initData); + ZkBobPoolETH pool = ZkBobPoolETH(address(poolProxy)); + + ZkBobDirectDepositQueueETH queueImpl = new ZkBobDirectDepositQueueETH(address(pool), weth); + queueProxy.upgradeTo(address(queueImpl)); + ZkBobDirectDepositQueueETH queue = ZkBobDirectDepositQueueETH(address(queueProxy)); + + IOperatorManager operatorManager = + new MutableOperatorManager(zkBobRelayer, zkBobRelayerFeeReceiver, zkBobRelayerURL); + pool.setOperatorManager(operatorManager); + queue.setOperatorManager(operatorManager); + + if (owner != address(0)) { + pool.transferOwnership(owner); + queue.transferOwnership(owner); + } + + if (admin != tx.origin) { + poolProxy.setAdmin(admin); + queueProxy.setAdmin(admin); + } + + vm.stopBroadcast(); + + require(poolProxy.implementation() == address(poolImpl), "Invalid implementation address"); + require(poolProxy.admin() == admin, "Proxy admin is not configured"); + require(pool.owner() == owner, "Owner is not configured"); + require(queueProxy.implementation() == address(queueImpl), "Invalid implementation address"); + require(queueProxy.admin() == admin, "Proxy admin is not configured"); + require(queue.owner() == owner, "Owner is not configured"); + require(pool.transfer_verifier() == transferVerifier, "Transfer verifier is not configured"); + require(pool.tree_verifier() == treeVerifier, "Tree verifier is not configured"); + require(pool.batch_deposit_verifier() == batchDepositVerifier, "Batch deposit verifier is not configured"); + + console2.log("ZkBobPool:", address(pool)); + console2.log("ZkBobPool implementation:", address(poolImpl)); + console2.log("ZkBobDirectDepositQueue:", address(queue)); + console2.log("ZkBobDirectDepositQueue implementation:", address(queueImpl)); + console2.log("TransferVerifier:", address(transferVerifier)); + console2.log("TreeUpdateVerifier:", address(treeVerifier)); + console2.log("BatchDepositVierifier:", address(batchDepositVerifier)); + } +} diff --git a/src/interfaces/IZkBobDirectDeposits.sol b/src/interfaces/IZkBobDirectDeposits.sol index 7002c56..51caf36 100644 --- a/src/interfaces/IZkBobDirectDeposits.sol +++ b/src/interfaces/IZkBobDirectDeposits.sol @@ -60,36 +60,6 @@ interface IZkBobDirectDeposits { external returns (uint256 depositId); - /** - * @notice Performs a direct deposit to the specified zk address in native token. - * In case the deposit cannot be processed, it can be refunded later to the fallbackReceiver address. - * @param fallbackReceiver receiver of deposit refund. - * @param zkAddress receiver zk address. - * @return depositId id of the submitted deposit to query status for. - */ - function directNativeDeposit( - address fallbackReceiver, - bytes memory zkAddress - ) - external - payable - returns (uint256 depositId); - - /** - * @notice Performs a direct deposit to the specified zk address in native token. - * In case the deposit cannot be processed, it can be refunded later to the fallbackReceiver address. - * @param fallbackReceiver receiver of deposit refund. - * @param zkAddress receiver zk address. - * @return depositId id of the submitted deposit to query status for. - */ - function directNativeDeposit( - address fallbackReceiver, - string memory zkAddress - ) - external - payable - returns (uint256 depositId); - /** * @notice ERC677 callback for performing a direct deposit. * Do not call this function directly, it's only intended to be called by the token contract. diff --git a/src/interfaces/IZkBobDirectDepositsETH.sol b/src/interfaces/IZkBobDirectDepositsETH.sol new file mode 100644 index 0000000..23804ed --- /dev/null +++ b/src/interfaces/IZkBobDirectDepositsETH.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "./IZkBobDirectDeposits.sol"; + +interface IZkBobDirectDepositsETH is IZkBobDirectDeposits { + /** + * @notice Performs a direct deposit to the specified zk address in native token. + * In case the deposit cannot be processed, it can be refunded later to the fallbackReceiver address. + * @param fallbackReceiver receiver of deposit refund. + * @param zkAddress receiver zk address. + * @return depositId id of the submitted deposit to query status for. + */ + function directNativeDeposit( + address fallbackReceiver, + bytes memory zkAddress + ) + external + payable + returns (uint256 depositId); + + /** + * @notice Performs a direct deposit to the specified zk address in native token. + * In case the deposit cannot be processed, it can be refunded later to the fallbackReceiver address. + * @param fallbackReceiver receiver of deposit refund. + * @param zkAddress receiver zk address. + * @return depositId id of the submitted deposit to query status for. + */ + function directNativeDeposit( + address fallbackReceiver, + string memory zkAddress + ) + external + payable + returns (uint256 depositId); +} diff --git a/src/zkbob/ZkBobDirectDepositQueue.sol b/src/zkbob/ZkBobDirectDepositQueue.sol index d7a5d99..8fa5d1d 100644 --- a/src/zkbob/ZkBobDirectDepositQueue.sol +++ b/src/zkbob/ZkBobDirectDepositQueue.sol @@ -187,25 +187,6 @@ contract ZkBobDirectDepositQueue is IZkBobDirectDeposits, IZkBobDirectDepositQue return _recordDirectDeposit(msg.sender, _fallbackUser, _amount, _rawZkAddress); } - /// @inheritdoc IZkBobDirectDeposits - function directNativeDeposit( - address _fallbackUser, - string calldata _zkAddress - ) - external - payable - returns (uint256) - { - return directNativeDeposit(_fallbackUser, bytes(_zkAddress)); - } - - /// @inheritdoc IZkBobDirectDeposits - function directNativeDeposit(address _fallbackUser, bytes memory _rawZkAddress) public payable returns (uint256) { - uint256 amount = msg.value; - IWETH9(token).deposit{value: amount}(); - return _recordDirectDeposit(msg.sender, _fallbackUser, amount, _rawZkAddress); - } - /// @inheritdoc IZkBobDirectDeposits function onTokenTransfer(address _from, uint256 _value, bytes calldata _data) external returns (bool) { require(msg.sender == token, "ZkBobDirectDepositQueue: not a token caller"); diff --git a/src/zkbob/ZkBobDirectDepositQueueETH.sol b/src/zkbob/ZkBobDirectDepositQueueETH.sol new file mode 100644 index 0000000..9bc8e07 --- /dev/null +++ b/src/zkbob/ZkBobDirectDepositQueueETH.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; +import "../libraries/ZkAddress.sol"; +import "../interfaces/IOperatorManager.sol"; +import "../interfaces/IZkBobDirectDepositsETH.sol"; +import "../interfaces/IZkBobDirectDepositQueue.sol"; +import "../interfaces/IZkBobPool.sol"; +import "../utils/Ownable.sol"; +import "../proxy/EIP1967Admin.sol"; +import "./ZkBobDirectDepositQueue.sol"; + +/** + * @title ZkBobDirectDepositQueue + * Queue for zkBob direct deposits. + */ +contract ZkBobDirectDepositQueueETH is IZkBobDirectDepositsETH, ZkBobDirectDepositQueue { + constructor(address _pool, address _token) ZkBobDirectDepositQueue(_pool, _token) {} + + /// @inheritdoc IZkBobDirectDepositsETH + function directNativeDeposit( + address _fallbackUser, + string calldata _zkAddress + ) + external + payable + returns (uint256) + { + return directNativeDeposit(_fallbackUser, bytes(_zkAddress)); + } + + /// @inheritdoc IZkBobDirectDepositsETH + function directNativeDeposit(address _fallbackUser, bytes memory _rawZkAddress) public payable returns (uint256) { + uint256 amount = msg.value; + IWETH9(token).deposit{value: amount}(); + return _recordDirectDeposit(msg.sender, _fallbackUser, amount, _rawZkAddress); + } +} diff --git a/src/zkbob/ZkBobETHPool.sol b/src/zkbob/ZkBobETHPool.sol deleted file mode 100644 index 645fd68..0000000 --- a/src/zkbob/ZkBobETHPool.sol +++ /dev/null @@ -1,427 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.15; - -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryImmutableState.sol"; -import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; -import "../interfaces/ITransferVerifier.sol"; -import "../interfaces/ITreeVerifier.sol"; -import "../interfaces/IBatchDepositVerifier.sol"; -import "../interfaces/IMintableERC20.sol"; -import "../interfaces/IOperatorManager.sol"; -import "../interfaces/IERC20Permit.sol"; -import "../interfaces/IPermit2.sol"; -import "../interfaces/IZkBobDirectDepositQueue.sol"; -import "../interfaces/IZkBobPool.sol"; -import "./utils/Parameters.sol"; -import "./utils/ZkBobAccounting.sol"; -import "../utils/Ownable.sol"; -import "../proxy/EIP1967Admin.sol"; -import "../utils/Sacrifice.sol"; - -/** - * @title ZkBobETHPool - * Shielded transactions pool for native and wrappred native tokens. - */ -contract ZkBobETHPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccounting { - using SafeERC20 for IERC20; - - uint256 internal constant MAX_POOL_ID = 0xffffff; - uint256 internal constant TOKEN_DENOMINATOR = 1_000_000_000; - bytes4 internal constant MESSAGE_PREFIX_COMMON_V1 = 0x00000000; - - uint256 public immutable pool_id; - ITransferVerifier public immutable transfer_verifier; - ITreeVerifier public immutable tree_verifier; - IBatchDepositVerifier public immutable batch_deposit_verifier; - IWETH9 public immutable token; - IPermit2 public immutable permit2; - IZkBobDirectDepositQueue public immutable direct_deposit_queue; - - IOperatorManager public operatorManager; - - mapping(uint256 => uint256) public nullifiers; - mapping(uint256 => uint256) public roots; - bytes32 public all_messages_hash; - - mapping(address => uint256) public accumulatedFee; - - event UpdateOperatorManager(address manager); - event WithdrawFee(address indexed operator, uint256 fee); - - event Message(uint256 indexed index, bytes32 indexed hash, bytes message); - - constructor( - uint256 __pool_id, - address _token, - ITransferVerifier _transfer_verifier, - ITreeVerifier _tree_verifier, - IBatchDepositVerifier _batch_deposit_verifier, - address _direct_deposit_queue, - address _permit2 - ) { - require(__pool_id <= MAX_POOL_ID, "ZkBobPool: exceeds max pool id"); - require(Address.isContract(_token), "ZkBobPool: not a contract"); - require(Address.isContract(address(_transfer_verifier)), "ZkBobPool: not a contract"); - require(Address.isContract(address(_tree_verifier)), "ZkBobPool: not a contract"); - require(Address.isContract(_direct_deposit_queue), "ZkBobPool: not a contract"); - require(Address.isContract(_permit2), "ZkBobPool: not a contract"); - pool_id = __pool_id; - token = IWETH9(_token); - transfer_verifier = _transfer_verifier; - tree_verifier = _tree_verifier; - batch_deposit_verifier = _batch_deposit_verifier; - direct_deposit_queue = IZkBobDirectDepositQueue(_direct_deposit_queue); - permit2 = IPermit2(_permit2); - } - - /** - * @dev Throws if called by any account other than the current relayer operator. - */ - modifier onlyOperator() { - require(operatorManager.isOperator(_msgSender()), "ZkBobPool: not an operator"); - _; - } - - /** - * @dev Initializes pool proxy storage. - * Callable only once and only through EIP1967Proxy constructor / upgradeToAndCall. - * @param _root initial empty merkle tree root. - * @param _tvlCap initial upper cap on the entire pool tvl, 18 decimals. - * @param _dailyDepositCap initial daily limit on the sum of all deposits, 18 decimals. - * @param _dailyWithdrawalCap initial daily limit on the sum of all withdrawals, 18 decimals. - * @param _dailyUserDepositCap initial daily limit on the sum of all per-address deposits, 18 decimals. - * @param _depositCap initial limit on the amount of a single deposit, 18 decimals. - * @param _dailyUserDirectDepositCap initial daily limit on the sum of all per-address direct deposits, 18 decimals. - * @param _directDepositCap initial limit on the amount of a single direct deposit, 18 decimals. - */ - function initialize( - uint256 _root, - uint256 _tvlCap, - uint256 _dailyDepositCap, - uint256 _dailyWithdrawalCap, - uint256 _dailyUserDepositCap, - uint256 _depositCap, - uint256 _dailyUserDirectDepositCap, - uint256 _directDepositCap - ) - external - { - require(msg.sender == address(this), "ZkBobPool: not initializer"); - require(roots[0] == 0, "ZkBobPool: already initialized"); - require(_root != 0, "ZkBobPool: zero root"); - roots[0] = _root; - _setLimits( - 0, - _tvlCap / TOKEN_DENOMINATOR, - _dailyDepositCap / TOKEN_DENOMINATOR, - _dailyWithdrawalCap / TOKEN_DENOMINATOR, - _dailyUserDepositCap / TOKEN_DENOMINATOR, - _depositCap / TOKEN_DENOMINATOR, - _dailyUserDirectDepositCap / TOKEN_DENOMINATOR, - _directDepositCap / TOKEN_DENOMINATOR - ); - } - - /** - * @dev Updates used operator manager contract. - * Callable only by the contract owner / proxy admin. - * @param _operatorManager new operator manager implementation. - */ - function setOperatorManager(IOperatorManager _operatorManager) external onlyOwner { - require(address(_operatorManager) != address(0), "ZkBobPool: manager is zero address"); - operatorManager = _operatorManager; - emit UpdateOperatorManager(address(_operatorManager)); - } - - /** - * @dev Tells the denominator for converting BOB into zkBOB units. - * 1e18 BOB units = 1e9 zkBOB units. - */ - function denominator() external pure returns (uint256) { - return TOKEN_DENOMINATOR; - } - - /** - * @dev Tells the current merkle tree index, which will be used for the next operation. - * Each operation increases merkle tree size by 128, so index is equal to the total number of seen operations, multiplied by 128. - * @return next operator merkle index. - */ - function pool_index() external view returns (uint256) { - return _txCount() << 7; - } - - function _root() internal view override returns (uint256) { - return roots[_transfer_index()]; - } - - function _pool_id() internal view override returns (uint256) { - return pool_id; - } - - /** - * @dev Perform a zkBob pool transaction. - * Callable only by the current operator. - * Method uses a custom ABI encoding scheme described in CustomABIDecoder. - * Single transact() call performs either deposit, withdrawal or shielded transfer operation. - */ - function transact() external onlyOperator { - address user; - uint256 txType = _tx_type(); - if (txType == 0) { - user = _deposit_spender(); - } else if (txType == 2) { - user = _memo_receiver(); - } else if (txType == 3) { - user = _memo_permit_holder(); - } - int256 transfer_token_delta = _transfer_token_amount(); - (,, uint256 txCount) = _recordOperation(user, transfer_token_delta); - - uint256 nullifier = _transfer_nullifier(); - { - uint256 _pool_index = txCount << 7; - - require(nullifiers[nullifier] == 0, "ZkBobPool: doublespend detected"); - require(_transfer_index() <= _pool_index, "ZkBobPool: transfer index out of bounds"); - require(transfer_verifier.verifyProof(_transfer_pub(), _transfer_proof()), "ZkBobPool: bad transfer proof"); - require( - tree_verifier.verifyProof(_tree_pub(roots[_pool_index]), _tree_proof()), "ZkBobPool: bad tree proof" - ); - - nullifiers[nullifier] = uint256(keccak256(abi.encodePacked(_transfer_out_commit(), _transfer_delta()))); - _pool_index += 128; - roots[_pool_index] = _tree_root_after(); - bytes memory message = _memo_message(); - // restrict memo message prefix (items count in little endian) to be < 2**16 - require(bytes4(message) & 0x0000ffff == MESSAGE_PREFIX_COMMON_V1, "ZkBobPool: bad message prefix"); - bytes32 message_hash = keccak256(message); - bytes32 _all_messages_hash = keccak256(abi.encodePacked(all_messages_hash, message_hash)); - all_messages_hash = _all_messages_hash; - emit Message(_pool_index, _all_messages_hash, message); - } - - uint256 fee = _memo_fee(); - int256 token_amount = transfer_token_delta + int256(fee); - int256 energy_amount = _transfer_energy_amount(); - - if (txType == 0) { - // Deposit - require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - IERC20(token).safeTransferFrom(user, address(this), uint256(token_amount) * TOKEN_DENOMINATOR); - } else if (txType == 1) { - // Transfer - require(token_amount == 0 && energy_amount == 0, "ZkBobPool: incorrect transfer amounts"); - } else if (txType == 2) { - // Withdraw - require(token_amount <= 0 && energy_amount <= 0, "ZkBobPool: incorrect withdraw amounts"); - - uint256 native_amount = _memo_native_amount() * TOKEN_DENOMINATOR; - uint256 withdraw_amount = uint256(-token_amount) * TOKEN_DENOMINATOR; - - if (native_amount > 0) { - token.withdraw(native_amount); - if (!payable(user).send(native_amount)) { - new Sacrifice{value: native_amount}(user); - } - withdraw_amount = withdraw_amount - native_amount; - } - - if (withdraw_amount > 0) { - IERC20(token).safeTransfer(user, withdraw_amount); - } - - // energy withdrawals are not yet implemented, any transaction with non-zero energy_amount will revert - // future version of the protocol will support energy withdrawals through negative energy_amount - if (energy_amount < 0) { - revert("ZkBobPool: XP claiming is not yet enabled"); - } - } else if (txType == 3) { - // Permittable token deposit - require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); - - bytes memory depositSignature = new bytes(65); - - assembly { - mstore(add(depositSignature, 0x20), r) - mstore(add(depositSignature, 0x40), s) - mstore8(add(depositSignature, 0x60), v) - } - - permit2.permitTransferFrom( - IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({ - token: address(token), - amount: uint256(token_amount) * TOKEN_DENOMINATOR - }), - nonce: nullifier, - deadline: uint256(_memo_permit_deadline()) - }), - IPermit2.SignatureTransferDetails({ - to: address(this), - requestedAmount: uint256(token_amount) * TOKEN_DENOMINATOR - }), - user, - depositSignature - ); - } else { - revert("ZkBobPool: Incorrect transaction type"); - } - - if (fee > 0) { - accumulatedFee[msg.sender] += fee; - } - } - - /** - * @dev Appends a batch of direct deposits into a zkBob merkle tree. - * Callable only by the current operator. - * @param _root_after new merkle tree root after append. - * @param _indices list of indices for queued pending deposits. - * @param _out_commit out commitment for output notes serialized from direct deposits. - * @param _batch_deposit_proof snark proof for batch deposit verifier. - * @param _tree_proof snark proof for tree update verifier. - */ - function appendDirectDeposits( - uint256 _root_after, - uint256[] calldata _indices, - uint256 _out_commit, - uint256[8] memory _batch_deposit_proof, - uint256[8] memory _tree_proof - ) - external - onlyOperator - { - (uint256 total, uint256 totalFee, uint256 hashsum, bytes memory message) = - direct_deposit_queue.collect(_indices, _out_commit); - - uint256 txCount = _processDirectDepositBatch(total); - uint256 _pool_index = txCount << 7; - - // verify that _out_commit corresponds to zero output account + 16 chosen notes + 111 empty notes - require( - batch_deposit_verifier.verifyProof([hashsum], _batch_deposit_proof), "ZkBobPool: bad batch deposit proof" - ); - - uint256[3] memory tree_pub = [roots[_pool_index], _root_after, _out_commit]; - require(tree_verifier.verifyProof(tree_pub, _tree_proof), "ZkBobPool: bad tree proof"); - - _pool_index += 128; - roots[_pool_index] = _root_after; - bytes32 message_hash = keccak256(message); - bytes32 _all_messages_hash = keccak256(abi.encodePacked(all_messages_hash, message_hash)); - all_messages_hash = _all_messages_hash; - - if (totalFee > 0) { - accumulatedFee[msg.sender] += totalFee; - } - - emit Message(_pool_index, _all_messages_hash, message); - } - - /** - * @dev Records submitted direct deposit into the users limits. - * Callable only by the direct deposit queue. - * @param _sender direct deposit sender. - * @param _amount direct deposit amount in zkBOB units. - */ - function recordDirectDeposit(address _sender, uint256 _amount) external { - require(msg.sender == address(direct_deposit_queue), "ZkBobPool: not authorized"); - _checkDirectDepositLimits(_sender, _amount); - } - - /** - * @dev Withdraws accumulated fee on behalf of an operator. - * Callable only by the operator itself, or by a pre-configured operator fee receiver address. - * @param _operator address of an operator account to withdraw fee from. - * @param _to address of the accumulated fee tokens receiver. - */ - function withdrawFee(address _operator, address _to) external { - require( - _operator == msg.sender || operatorManager.isOperatorFeeReceiver(_operator, msg.sender), - "ZkBobPool: not authorized" - ); - uint256 fee = accumulatedFee[_operator] * TOKEN_DENOMINATOR; - require(fee > 0, "ZkBobPool: no fee to withdraw"); - IERC20(token).safeTransfer(_to, fee); - accumulatedFee[_operator] = 0; - emit WithdrawFee(_operator, fee); - } - - /** - * @dev Updates pool usage limits. - * Callable only by the contract owner / proxy admin. - * @param _tier pool limits tier (0-254). - * @param _tvlCap new upper cap on the entire pool tvl, 18 decimals. - * @param _dailyDepositCap new daily limit on the sum of all deposits, 18 decimals. - * @param _dailyWithdrawalCap new daily limit on the sum of all withdrawals, 18 decimals. - * @param _dailyUserDepositCap new daily limit on the sum of all per-address deposits, 18 decimals. - * @param _depositCap new limit on the amount of a single deposit, 18 decimals. - * @param _dailyUserDirectDepositCap new daily limit on the sum of all per-address direct deposits, 18 decimals. - * @param _directDepositCap new limit on the amount of a single direct deposit, 18 decimals. - */ - function setLimits( - uint8 _tier, - uint256 _tvlCap, - uint256 _dailyDepositCap, - uint256 _dailyWithdrawalCap, - uint256 _dailyUserDepositCap, - uint256 _depositCap, - uint256 _dailyUserDirectDepositCap, - uint256 _directDepositCap - ) - external - onlyOwner - { - _setLimits( - _tier, - _tvlCap / TOKEN_DENOMINATOR, - _dailyDepositCap / TOKEN_DENOMINATOR, - _dailyWithdrawalCap / TOKEN_DENOMINATOR, - _dailyUserDepositCap / TOKEN_DENOMINATOR, - _depositCap / TOKEN_DENOMINATOR, - _dailyUserDirectDepositCap / TOKEN_DENOMINATOR, - _directDepositCap / TOKEN_DENOMINATOR - ); - } - - /** - * @dev Resets daily limit usage for the current day. - * Callable only by the contract owner / proxy admin. - * @param _tier tier id to reset daily limits for. - */ - function resetDailyLimits(uint8 _tier) external onlyOwner { - _resetDailyLimits(_tier); - } - - /** - * @dev Updates users limit tiers. - * Callable only by the contract owner / proxy admin. - * @param _tier pool limits tier (0-255). - * 0 is the default tier. - * 1-254 are custom pool limit tiers, configured at runtime. - * 255 is the special tier with zero limits, used to effectively prevent some address from accessing the pool. - * @param _users list of user account addresses to assign a tier for. - */ - function setUsersTier(uint8 _tier, address[] memory _users) external onlyOwner { - _setUsersTier(_tier, _users); - } - - /** - * @dev Tells if caller is the contract owner. - * Gives ownership rights to the proxy admin as well. - * @return true, if caller is the contract owner or proxy admin. - */ - function _isOwner() internal view override returns (bool) { - return super._isOwner() || _admin() == _msgSender(); - } - - receive() external payable { - require(msg.sender == address(token), "Not a WETH withdrawal"); - } -} diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index fe55dd6..d9af88c 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -26,7 +26,7 @@ import "../proxy/EIP1967Admin.sol"; * @title ZkBobPool * Shielded transactions pool for BOB tokens. */ -contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccounting { +abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccounting { using SafeERC20 for IERC20; uint256 internal constant MAX_POOL_ID = 0xffffff; @@ -48,9 +48,6 @@ contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccoun mapping(address => uint256) public accumulatedFee; - ITokenSeller public tokenSeller; - - event UpdateTokenSeller(address seller); event UpdateOperatorManager(address manager); event WithdrawFee(address indexed operator, uint256 fee); @@ -125,16 +122,6 @@ contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccoun ); } - /** - * @dev Updates token seller contract used for native coin withdrawals. - * Callable only by the contract owner / proxy admin. - * @param _seller new token seller contract implementation. address(0) will deactivate native withdrawals. - */ - function setTokenSeller(address _seller) external onlyOwner { - tokenSeller = ITokenSeller(_seller); - emit UpdateTokenSeller(_seller); - } - /** * @dev Updates used operator manager contract. * Callable only by the contract owner / proxy admin. @@ -171,6 +158,24 @@ contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccoun return pool_id; } + /** + * @dev Withdraws native token + * Callable only by transact method + * @param user Address of receiver + * @param tokenAmount Amount of token to withdraw + * @return spentAmount Actual spent amount + */ + function _withdrawNative(address user, uint256 tokenAmount) internal virtual returns (uint256 spentAmount); + + /** + * @dev Finalizes permit deposit + * Callable only by transact method + * @param user Address of depositor + * @param nullifier Nullifier + * @param tokenAmount Amount of token to deposit + */ + function _finalizePermitDeposit(address user, uint256 nullifier, int256 tokenAmount) internal virtual; + /** * @dev Perform a zkBob pool transaction. * Callable only by the current operator. @@ -232,12 +237,13 @@ contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccoun uint256 withdraw_amount = uint256(-token_amount) * TOKEN_DENOMINATOR; if (native_amount > 0) { - ITokenSeller seller = tokenSeller; - if (address(seller) != address(0)) { - IERC20(token).safeTransfer(address(seller), native_amount); - (, uint256 refunded) = seller.sellForETH(user, native_amount); - withdraw_amount = withdraw_amount - native_amount + refunded; - } + withdraw_amount -= _withdrawNative(user, native_amount); + // ITokenSeller seller = tokenSeller; + // if (address(seller) != address(0)) { + // IERC20(token).safeTransfer(address(seller), native_amount); + // (, uint256 refunded) = seller.sellForETH(user, native_amount); + // withdraw_amount = withdraw_amount - native_amount + refunded; + // } } if (withdraw_amount > 0) { @@ -252,10 +258,11 @@ contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ZkBobAccoun } else if (txType == 3) { // Permittable token deposit require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); - IERC20Permit(token).receiveWithSaltedPermit( - user, uint256(token_amount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(nullifier), v, r, s - ); + _finalizePermitDeposit(user, nullifier, token_amount); + // (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); + // IERC20Permit(token).receiveWithSaltedPermit( + // user, uint256(token_amount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(nullifier), v, r, s + // ); } else { revert("ZkBobPool: Incorrect transaction type"); } diff --git a/src/zkbob/ZkBobPoolERC20.sol b/src/zkbob/ZkBobPoolERC20.sol new file mode 100644 index 0000000..0176777 --- /dev/null +++ b/src/zkbob/ZkBobPoolERC20.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryImmutableState.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; +import "../interfaces/ITransferVerifier.sol"; +import "../interfaces/ITreeVerifier.sol"; +import "../interfaces/IBatchDepositVerifier.sol"; +import "../interfaces/IMintableERC20.sol"; +import "../interfaces/IOperatorManager.sol"; +import "../interfaces/IERC20Permit.sol"; +import "../interfaces/ITokenSeller.sol"; +import "../interfaces/IZkBobDirectDepositQueue.sol"; +import "../interfaces/IZkBobPool.sol"; +import "./utils/Parameters.sol"; +import "./utils/ZkBobAccounting.sol"; +import "../utils/Ownable.sol"; +import "../proxy/EIP1967Admin.sol"; +import "./ZkBobPool.sol"; + +/** + * @title ZkBobPool + * Shielded transactions pool for BOB tokens. + */ +contract ZkBobPoolERC20 is ZkBobPool { + using SafeERC20 for IERC20; + + ITokenSeller public tokenSeller; + + event UpdateTokenSeller(address seller); + + constructor( + uint256 __pool_id, + address _token, + ITransferVerifier _transfer_verifier, + ITreeVerifier _tree_verifier, + IBatchDepositVerifier _batch_deposit_verifier, + address _direct_deposit_queue + ) + ZkBobPool(__pool_id, _token, _transfer_verifier, _tree_verifier, _batch_deposit_verifier, _direct_deposit_queue) + {} + + /** + * @dev Updates token seller contract used for native coin withdrawals. + * Callable only by the contract owner / proxy admin. + * @param _seller new token seller contract implementation. address(0) will deactivate native withdrawals. + */ + function setTokenSeller(address _seller) external onlyOwner { + tokenSeller = ITokenSeller(_seller); + emit UpdateTokenSeller(_seller); + } + + // @inheritdoc ZkBobPool + function _withdrawNative(address user, uint256 tokenAmount) internal override returns (uint256 spentAmount) { + ITokenSeller seller = tokenSeller; + if (address(seller) != address(0)) { + IERC20(token).safeTransfer(address(seller), tokenAmount); + (, uint256 refunded) = seller.sellForETH(user, tokenAmount); + return tokenAmount - refunded; + } + } + + // @inheritdoc ZkBobPool + function _finalizePermitDeposit(address user, uint256 nullifier, int256 tokenAmount) internal override { + (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); + IERC20Permit(token).receiveWithSaltedPermit( + user, uint256(tokenAmount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(nullifier), v, r, s + ); + } +} diff --git a/src/zkbob/ZkBobPoolETH.sol b/src/zkbob/ZkBobPoolETH.sol new file mode 100644 index 0000000..365bb36 --- /dev/null +++ b/src/zkbob/ZkBobPoolETH.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryImmutableState.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; +import "../interfaces/ITransferVerifier.sol"; +import "../interfaces/ITreeVerifier.sol"; +import "../interfaces/IBatchDepositVerifier.sol"; +import "../interfaces/IMintableERC20.sol"; +import "../interfaces/IOperatorManager.sol"; +import "../interfaces/IERC20Permit.sol"; +import "../interfaces/IPermit2.sol"; +import "../interfaces/IZkBobDirectDepositQueue.sol"; +import "../interfaces/IZkBobPool.sol"; +import "./utils/Parameters.sol"; +import "./utils/ZkBobAccounting.sol"; +import "../utils/Ownable.sol"; +import "../proxy/EIP1967Admin.sol"; +import "../utils/Sacrifice.sol"; +import "./ZkBobPool.sol"; + +/** + * @title ZkBobETHPool + * Shielded transactions pool for native and wrappred native tokens. + */ +contract ZkBobPoolETH is ZkBobPool { + using SafeERC20 for IERC20; + + IPermit2 public immutable permit2; + + constructor( + uint256 __pool_id, + address _token, + ITransferVerifier _transfer_verifier, + ITreeVerifier _tree_verifier, + IBatchDepositVerifier _batch_deposit_verifier, + address _direct_deposit_queue, + address _permit2 + ) + ZkBobPool(__pool_id, _token, _transfer_verifier, _tree_verifier, _batch_deposit_verifier, _direct_deposit_queue) + { + require(Address.isContract(_permit2), "ZkBobPool: not a contract"); + permit2 = IPermit2(_permit2); + } + + // @inheritdoc ZkBobPool + function _withdrawNative(address user, uint256 tokenAmount) internal override returns (uint256 spentAmount) { + IWETH9(token).withdraw(tokenAmount); + if (!payable(user).send(tokenAmount)) { + new Sacrifice{value: tokenAmount}(user); + } + return tokenAmount; + } + + // @inheritdoc ZkBobPool + function _finalizePermitDeposit(address user, uint256 nullifier, int256 tokenAmount) internal override { + (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); + + bytes memory depositSignature = new bytes(65); + + assembly { + mstore(add(depositSignature, 0x20), r) + mstore(add(depositSignature, 0x40), s) + mstore8(add(depositSignature, 0x60), v) + } + + permit2.permitTransferFrom( + IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: token, amount: uint256(tokenAmount) * TOKEN_DENOMINATOR}), + nonce: nullifier, + deadline: uint256(_memo_permit_deadline()) + }), + IPermit2.SignatureTransferDetails({ + to: address(this), + requestedAmount: uint256(tokenAmount) * TOKEN_DENOMINATOR + }), + user, + depositSignature + ); + } + + // /** + // * @dev Perform a zkBob pool transaction. + // * Callable only by the current operator. + // * Method uses a custom ABI encoding scheme described in CustomABIDecoder. + // * Single transact() call performs either deposit, withdrawal or shielded transfer operation. + // */ + // function transact() external onlyOperator { + // address user; + // uint256 txType = _tx_type(); + // if (txType == 0) { + // user = _deposit_spender(); + // } else if (txType == 2) { + // user = _memo_receiver(); + // } else if (txType == 3) { + // user = _memo_permit_holder(); + // } + // int256 transfer_token_delta = _transfer_token_amount(); + // (,, uint256 txCount) = _recordOperation(user, transfer_token_delta); + // + // uint256 nullifier = _transfer_nullifier(); + // { + // uint256 _pool_index = txCount << 7; + // + // require(nullifiers[nullifier] == 0, "ZkBobPool: doublespend detected"); + // require(_transfer_index() <= _pool_index, "ZkBobPool: transfer index out of bounds"); + // require(transfer_verifier.verifyProof(_transfer_pub(), _transfer_proof()), "ZkBobPool: bad transfer proof"); + // require( + // tree_verifier.verifyProof(_tree_pub(roots[_pool_index]), _tree_proof()), "ZkBobPool: bad tree proof" + // ); + // + // nullifiers[nullifier] = uint256(keccak256(abi.encodePacked(_transfer_out_commit(), _transfer_delta()))); + // _pool_index += 128; + // roots[_pool_index] = _tree_root_after(); + // bytes memory message = _memo_message(); + // // restrict memo message prefix (items count in little endian) to be < 2**16 + // require(bytes4(message) & 0x0000ffff == MESSAGE_PREFIX_COMMON_V1, "ZkBobPool: bad message prefix"); + // bytes32 message_hash = keccak256(message); + // bytes32 _all_messages_hash = keccak256(abi.encodePacked(all_messages_hash, message_hash)); + // all_messages_hash = _all_messages_hash; + // emit Message(_pool_index, _all_messages_hash, message); + // } + // + // uint256 fee = _memo_fee(); + // int256 token_amount = transfer_token_delta + int256(fee); + // int256 energy_amount = _transfer_energy_amount(); + // + // if (txType == 0) { + // // Deposit + // require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); + // IERC20(token).safeTransferFrom(user, address(this), uint256(token_amount) * TOKEN_DENOMINATOR); + // } else if (txType == 1) { + // // Transfer + // require(token_amount == 0 && energy_amount == 0, "ZkBobPool: incorrect transfer amounts"); + // } else if (txType == 2) { + // // Withdraw + // require(token_amount <= 0 && energy_amount <= 0, "ZkBobPool: incorrect withdraw amounts"); + // + // uint256 native_amount = _memo_native_amount() * TOKEN_DENOMINATOR; + // uint256 withdraw_amount = uint256(-token_amount) * TOKEN_DENOMINATOR; + // + // if (native_amount > 0) { + // token.withdraw(native_amount); + // if (!payable(user).send(native_amount)) { + // new Sacrifice{value: native_amount}(user); + // } + // withdraw_amount = withdraw_amount - native_amount; + // } + // + // if (withdraw_amount > 0) { + // IERC20(token).safeTransfer(user, withdraw_amount); + // } + // + // // energy withdrawals are not yet implemented, any transaction with non-zero energy_amount will revert + // // future version of the protocol will support energy withdrawals through negative energy_amount + // if (energy_amount < 0) { + // revert("ZkBobPool: XP claiming is not yet enabled"); + // } + // } else if (txType == 3) { + // // Permittable token deposit + // require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); + // (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); + // + // bytes memory depositSignature = new bytes(65); + // + // assembly { + // mstore(add(depositSignature, 0x20), r) + // mstore(add(depositSignature, 0x40), s) + // mstore8(add(depositSignature, 0x60), v) + // } + // + // permit2.permitTransferFrom( + // IPermit2.PermitTransferFrom({ + // permitted: IPermit2.TokenPermissions({ + // token: address(token), + // amount: uint256(token_amount) * TOKEN_DENOMINATOR + // }), + // nonce: nullifier, + // deadline: uint256(_memo_permit_deadline()) + // }), + // IPermit2.SignatureTransferDetails({ + // to: address(this), + // requestedAmount: uint256(token_amount) * TOKEN_DENOMINATOR + // }), + // user, + // depositSignature + // ); + // } else { + // revert("ZkBobPool: Incorrect transaction type"); + // } + // + // if (fee > 0) { + // accumulatedFee[msg.sender] += fee; + // } + // } + + receive() external payable { + require(msg.sender == address(token), "Not a WETH withdrawal"); + } +} diff --git a/test/zkbob/ZkBobPool.t.sol b/test/zkbob/ZkBobPoolERC20.t.sol similarity index 98% rename from test/zkbob/ZkBobPool.t.sol rename to test/zkbob/ZkBobPoolERC20.t.sol index 6cebe00..e8af240 100644 --- a/test/zkbob/ZkBobPool.t.sol +++ b/test/zkbob/ZkBobPoolERC20.t.sol @@ -13,6 +13,7 @@ import "../mocks/BatchDepositVerifierMock.sol"; import "../mocks/DummyImpl.sol"; import "../../src/proxy/EIP1967Proxy.sol"; import "../../src/zkbob/ZkBobPool.sol"; +import "../../src/zkbob/ZkBobPoolERC20.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; import "../../src/BobToken.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; @@ -20,7 +21,7 @@ import "../../src/utils/UniswapV3Seller.sol"; import "../shared/ForkTests.t.sol"; import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; -contract ZkBobPoolTest is AbstractMainnetForkTest { +contract ZkBobPoolERC20Test is AbstractMainnetForkTest { uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; @@ -30,7 +31,7 @@ contract ZkBobPoolTest is AbstractMainnetForkTest { bytes constant zkAddress = "QsnTijXekjRm9hKcq5kLNPsa6P4HtMRrc3RxVx3jsLHeo2AiysYxVJP86mriHfN"; - ZkBobPool pool; + ZkBobPoolERC20 pool; ZkBobDirectDepositQueue queue; BobToken bob; IOperatorManager operatorManager; @@ -57,8 +58,8 @@ contract ZkBobPoolTest is AbstractMainnetForkTest { EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); - ZkBobPool impl = - new ZkBobPool(0, address(bob), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy)); + ZkBobPoolERC20 impl = + new ZkBobPoolERC20(0, address(bob), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy)); bytes memory initData = abi.encodeWithSelector( ZkBobPool.initialize.selector, @@ -72,7 +73,7 @@ contract ZkBobPoolTest is AbstractMainnetForkTest { 0 ); poolProxy.upgradeToAndCall(address(impl), initData); - pool = ZkBobPool(address(poolProxy)); + pool = ZkBobPoolERC20(address(poolProxy)); ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(bob)); queueProxy.upgradeTo(address(queueImpl)); diff --git a/test/zkbob/ZkBobETHPool.t.sol b/test/zkbob/ZkBobPoolETH.t.sol similarity index 95% rename from test/zkbob/ZkBobETHPool.t.sol rename to test/zkbob/ZkBobPoolETH.t.sol index 3c6dd14..569ab71 100644 --- a/test/zkbob/ZkBobETHPool.t.sol +++ b/test/zkbob/ZkBobPoolETH.t.sol @@ -12,15 +12,16 @@ import "../mocks/TreeUpdateVerifierMock.sol"; import "../mocks/BatchDepositVerifierMock.sol"; import "../mocks/DummyImpl.sol"; import "../../src/proxy/EIP1967Proxy.sol"; -import "../../src/zkbob/ZkBobETHPool.sol"; +import "../../src/zkbob/ZkBobPoolETH.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; import "../../src/BobToken.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; import "../shared/ForkTests.t.sol"; import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; -contract ZkBobETHPoolTest is AbstractMainnetForkTest { +contract ZkBobPoolETHTest is AbstractMainnetForkTest { uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; @@ -37,8 +38,8 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" ); - ZkBobETHPool pool; - ZkBobDirectDepositQueue queue; + ZkBobPoolETH pool; + ZkBobDirectDepositQueueETH queue; IWETH9 token; IOperatorManager operatorManager; IPermit2 permit2; @@ -64,11 +65,11 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); console2.log(weth, address(token)); - ZkBobETHPool impl = - new ZkBobETHPool(0, address(token), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy), permit2Address); + ZkBobPoolETH impl = + new ZkBobPoolETH(0, address(token), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy), permit2Address); bytes memory initData = abi.encodeWithSelector( - ZkBobETHPool.initialize.selector, + ZkBobPool.initialize.selector, initialRoot, 1_000_000 ether, 100_000 ether, @@ -79,11 +80,11 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { 0 ); poolProxy.upgradeToAndCall(address(impl), initData); - pool = ZkBobETHPool(payable(address(poolProxy))); + pool = ZkBobPoolETH(payable(address(poolProxy))); - ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(token)); + ZkBobDirectDepositQueueETH queueImpl = new ZkBobDirectDepositQueueETH(address(pool), address(token)); queueProxy.upgradeTo(address(queueImpl)); - queue = ZkBobDirectDepositQueue(address(queueProxy)); + queue = ZkBobDirectDepositQueueETH(address(queueProxy)); operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); pool.setOperatorManager(operatorManager); @@ -451,7 +452,7 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { (uint8 v, bytes32 r, bytes32 s) = _signSaltedPermit(pk1, user1, address(pool), uint256(_amount + int256(_fee)), expiry, nullifier); bytes memory data = abi.encodePacked( - ZkBobETHPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) + ZkBobPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) ); for (uint256 i = 0; i < 17; i++) { data = abi.encodePacked(data, _randFR()); @@ -466,7 +467,7 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { bytes32 nullifier = bytes32(_randFR()); (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, ECDSA.toEthSignedMessageHash(nullifier)); bytes memory data = abi.encodePacked( - ZkBobETHPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) + ZkBobPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) ); for (uint256 i = 0; i < 17; i++) { data = abi.encodePacked(data, _randFR()); @@ -477,7 +478,7 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { function _encodeWithdrawal(address _to, uint256 _amount, uint256 _nativeAmount) internal returns (bytes memory) { bytes memory data = abi.encodePacked( - ZkBobETHPool.transact.selector, + ZkBobPool.transact.selector, _randFR(), _randFR(), uint48(0), @@ -501,7 +502,7 @@ contract ZkBobETHPoolTest is AbstractMainnetForkTest { function _encodeTransfer() internal returns (bytes memory) { bytes memory data = abi.encodePacked( - ZkBobETHPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), int64(-0.01 ether / 1 gwei) + ZkBobPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), int64(-0.01 ether / 1 gwei) ); for (uint256 i = 0; i < 17; i++) { data = abi.encodePacked(data, _randFR()); From 590f8f7f6473340190b2053832eb51f53718e3ed Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Fri, 31 Mar 2023 19:09:11 +0300 Subject: [PATCH 05/11] fix --- script/scripts/NewZkBobPoolETHImpl.s.sol | 4 ++-- script/scripts/ZkBobPoolETH.s.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/scripts/NewZkBobPoolETHImpl.s.sol b/script/scripts/NewZkBobPoolETHImpl.s.sol index 619ee1b..f4a5220 100644 --- a/script/scripts/NewZkBobPoolETHImpl.s.sol +++ b/script/scripts/NewZkBobPoolETHImpl.s.sol @@ -13,7 +13,7 @@ contract DeployNewZkBobPoolETHImpl is Script { function run() external { vm.startBroadcast(); - ZkBobPoolETH pool = ZkBobPoolETH(zkBobPool); + ZkBobPoolETH pool = ZkBobPoolETH(payable(zkBobPool)); ZkBobPoolETH impl = new ZkBobPoolETH( pool.pool_id(), @@ -22,7 +22,7 @@ contract DeployNewZkBobPoolETHImpl is Script { pool.tree_verifier(), pool.batch_deposit_verifier(), address(pool.direct_deposit_queue()), - address(pool.permit2) + address(pool.permit2()) ); vm.stopBroadcast(); diff --git a/script/scripts/ZkBobPoolETH.s.sol b/script/scripts/ZkBobPoolETH.s.sol index 6ef3383..6eec9bb 100644 --- a/script/scripts/ZkBobPoolETH.s.sol +++ b/script/scripts/ZkBobPoolETH.s.sol @@ -54,7 +54,7 @@ contract DeployZkBobPoolETH is Script { zkBobDirectDepositCap ); poolProxy.upgradeToAndCall(address(poolImpl), initData); - ZkBobPoolETH pool = ZkBobPoolETH(address(poolProxy)); + ZkBobPoolETH pool = ZkBobPoolETH(payable(address(poolProxy))); ZkBobDirectDepositQueueETH queueImpl = new ZkBobDirectDepositQueueETH(address(pool), weth); queueProxy.upgradeTo(address(queueImpl)); From 061efbde754fdd1de75428247ba5a30d7512dbc7 Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Tue, 4 Apr 2023 14:48:23 +0300 Subject: [PATCH 06/11] refactoring --- src/zkbob/ZkBobDirectDepositQueue.sol | 1 - src/zkbob/ZkBobDirectDepositQueueETH.sol | 4 +- src/zkbob/ZkBobPool.sol | 14 +- src/zkbob/ZkBobPoolERC20.sol | 2 +- src/zkbob/ZkBobPoolETH.sol | 117 +---- test/interfaces/IZkBobDirectDepositsAdmin.sol | 15 + test/interfaces/IZkBobPoolAdmin.sol | 119 +++++ test/zkbob/ZkBobPool.t.sol | 423 +++++++++++++++++ test/zkbob/ZkBobPoolERC20.t.sol | 393 +--------------- test/zkbob/ZkBobPoolETH.t.sol | 424 +----------------- 10 files changed, 591 insertions(+), 921 deletions(-) create mode 100644 test/interfaces/IZkBobDirectDepositsAdmin.sol create mode 100644 test/interfaces/IZkBobPoolAdmin.sol create mode 100644 test/zkbob/ZkBobPool.t.sol diff --git a/src/zkbob/ZkBobDirectDepositQueue.sol b/src/zkbob/ZkBobDirectDepositQueue.sol index 8fa5d1d..3583f42 100644 --- a/src/zkbob/ZkBobDirectDepositQueue.sol +++ b/src/zkbob/ZkBobDirectDepositQueue.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.15; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; import "../libraries/ZkAddress.sol"; import "../interfaces/IOperatorManager.sol"; import "../interfaces/IZkBobDirectDeposits.sol"; diff --git a/src/zkbob/ZkBobDirectDepositQueueETH.sol b/src/zkbob/ZkBobDirectDepositQueueETH.sol index 9bc8e07..9c8b7c3 100644 --- a/src/zkbob/ZkBobDirectDepositQueueETH.sol +++ b/src/zkbob/ZkBobDirectDepositQueueETH.sol @@ -16,8 +16,8 @@ import "../proxy/EIP1967Admin.sol"; import "./ZkBobDirectDepositQueue.sol"; /** - * @title ZkBobDirectDepositQueue - * Queue for zkBob direct deposits. + * @title ZkBobDirectDepositQueueETH + * Queue for zkBob ETH direct deposits. */ contract ZkBobDirectDepositQueueETH is IZkBobDirectDepositsETH, ZkBobDirectDepositQueue { constructor(address _pool, address _token) ZkBobDirectDepositQueue(_pool, _token) {} diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index d9af88c..5ae410e 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -174,7 +174,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk * @param nullifier Nullifier * @param tokenAmount Amount of token to deposit */ - function _finalizePermitDeposit(address user, uint256 nullifier, int256 tokenAmount) internal virtual; + function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) internal virtual; /** * @dev Perform a zkBob pool transaction. @@ -238,12 +238,6 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk if (native_amount > 0) { withdraw_amount -= _withdrawNative(user, native_amount); - // ITokenSeller seller = tokenSeller; - // if (address(seller) != address(0)) { - // IERC20(token).safeTransfer(address(seller), native_amount); - // (, uint256 refunded) = seller.sellForETH(user, native_amount); - // withdraw_amount = withdraw_amount - native_amount + refunded; - // } } if (withdraw_amount > 0) { @@ -258,11 +252,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk } else if (txType == 3) { // Permittable token deposit require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - _finalizePermitDeposit(user, nullifier, token_amount); - // (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); - // IERC20Permit(token).receiveWithSaltedPermit( - // user, uint256(token_amount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(nullifier), v, r, s - // ); + _transferFromByPermit(user, nullifier, token_amount); } else { revert("ZkBobPool: Incorrect transaction type"); } diff --git a/src/zkbob/ZkBobPoolERC20.sol b/src/zkbob/ZkBobPoolERC20.sol index 0176777..a0193bd 100644 --- a/src/zkbob/ZkBobPoolERC20.sol +++ b/src/zkbob/ZkBobPoolERC20.sol @@ -66,7 +66,7 @@ contract ZkBobPoolERC20 is ZkBobPool { } // @inheritdoc ZkBobPool - function _finalizePermitDeposit(address user, uint256 nullifier, int256 tokenAmount) internal override { + function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) internal override { (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); IERC20Permit(token).receiveWithSaltedPermit( user, uint256(tokenAmount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(nullifier), v, r, s diff --git a/src/zkbob/ZkBobPoolETH.sol b/src/zkbob/ZkBobPoolETH.sol index 365bb36..3d6bcec 100644 --- a/src/zkbob/ZkBobPoolETH.sol +++ b/src/zkbob/ZkBobPoolETH.sol @@ -58,7 +58,7 @@ contract ZkBobPoolETH is ZkBobPool { } // @inheritdoc ZkBobPool - function _finalizePermitDeposit(address user, uint256 nullifier, int256 tokenAmount) internal override { + function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) internal override { (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); bytes memory depositSignature = new bytes(65); @@ -84,121 +84,6 @@ contract ZkBobPoolETH is ZkBobPool { ); } - // /** - // * @dev Perform a zkBob pool transaction. - // * Callable only by the current operator. - // * Method uses a custom ABI encoding scheme described in CustomABIDecoder. - // * Single transact() call performs either deposit, withdrawal or shielded transfer operation. - // */ - // function transact() external onlyOperator { - // address user; - // uint256 txType = _tx_type(); - // if (txType == 0) { - // user = _deposit_spender(); - // } else if (txType == 2) { - // user = _memo_receiver(); - // } else if (txType == 3) { - // user = _memo_permit_holder(); - // } - // int256 transfer_token_delta = _transfer_token_amount(); - // (,, uint256 txCount) = _recordOperation(user, transfer_token_delta); - // - // uint256 nullifier = _transfer_nullifier(); - // { - // uint256 _pool_index = txCount << 7; - // - // require(nullifiers[nullifier] == 0, "ZkBobPool: doublespend detected"); - // require(_transfer_index() <= _pool_index, "ZkBobPool: transfer index out of bounds"); - // require(transfer_verifier.verifyProof(_transfer_pub(), _transfer_proof()), "ZkBobPool: bad transfer proof"); - // require( - // tree_verifier.verifyProof(_tree_pub(roots[_pool_index]), _tree_proof()), "ZkBobPool: bad tree proof" - // ); - // - // nullifiers[nullifier] = uint256(keccak256(abi.encodePacked(_transfer_out_commit(), _transfer_delta()))); - // _pool_index += 128; - // roots[_pool_index] = _tree_root_after(); - // bytes memory message = _memo_message(); - // // restrict memo message prefix (items count in little endian) to be < 2**16 - // require(bytes4(message) & 0x0000ffff == MESSAGE_PREFIX_COMMON_V1, "ZkBobPool: bad message prefix"); - // bytes32 message_hash = keccak256(message); - // bytes32 _all_messages_hash = keccak256(abi.encodePacked(all_messages_hash, message_hash)); - // all_messages_hash = _all_messages_hash; - // emit Message(_pool_index, _all_messages_hash, message); - // } - // - // uint256 fee = _memo_fee(); - // int256 token_amount = transfer_token_delta + int256(fee); - // int256 energy_amount = _transfer_energy_amount(); - // - // if (txType == 0) { - // // Deposit - // require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - // IERC20(token).safeTransferFrom(user, address(this), uint256(token_amount) * TOKEN_DENOMINATOR); - // } else if (txType == 1) { - // // Transfer - // require(token_amount == 0 && energy_amount == 0, "ZkBobPool: incorrect transfer amounts"); - // } else if (txType == 2) { - // // Withdraw - // require(token_amount <= 0 && energy_amount <= 0, "ZkBobPool: incorrect withdraw amounts"); - // - // uint256 native_amount = _memo_native_amount() * TOKEN_DENOMINATOR; - // uint256 withdraw_amount = uint256(-token_amount) * TOKEN_DENOMINATOR; - // - // if (native_amount > 0) { - // token.withdraw(native_amount); - // if (!payable(user).send(native_amount)) { - // new Sacrifice{value: native_amount}(user); - // } - // withdraw_amount = withdraw_amount - native_amount; - // } - // - // if (withdraw_amount > 0) { - // IERC20(token).safeTransfer(user, withdraw_amount); - // } - // - // // energy withdrawals are not yet implemented, any transaction with non-zero energy_amount will revert - // // future version of the protocol will support energy withdrawals through negative energy_amount - // if (energy_amount < 0) { - // revert("ZkBobPool: XP claiming is not yet enabled"); - // } - // } else if (txType == 3) { - // // Permittable token deposit - // require(transfer_token_delta > 0 && energy_amount == 0, "ZkBobPool: incorrect deposit amounts"); - // (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); - // - // bytes memory depositSignature = new bytes(65); - // - // assembly { - // mstore(add(depositSignature, 0x20), r) - // mstore(add(depositSignature, 0x40), s) - // mstore8(add(depositSignature, 0x60), v) - // } - // - // permit2.permitTransferFrom( - // IPermit2.PermitTransferFrom({ - // permitted: IPermit2.TokenPermissions({ - // token: address(token), - // amount: uint256(token_amount) * TOKEN_DENOMINATOR - // }), - // nonce: nullifier, - // deadline: uint256(_memo_permit_deadline()) - // }), - // IPermit2.SignatureTransferDetails({ - // to: address(this), - // requestedAmount: uint256(token_amount) * TOKEN_DENOMINATOR - // }), - // user, - // depositSignature - // ); - // } else { - // revert("ZkBobPool: Incorrect transaction type"); - // } - // - // if (fee > 0) { - // accumulatedFee[msg.sender] += fee; - // } - // } - receive() external payable { require(msg.sender == address(token), "Not a WETH withdrawal"); } diff --git a/test/interfaces/IZkBobDirectDepositsAdmin.sol b/test/interfaces/IZkBobDirectDepositsAdmin.sol new file mode 100644 index 0000000..222575b --- /dev/null +++ b/test/interfaces/IZkBobDirectDepositsAdmin.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../src/interfaces/IZkBobDirectDeposits.sol"; +import "../../src/interfaces/IZkBobDirectDepositQueue.sol"; +import "../../src/interfaces/IZkBobDirectDepositsETH.sol"; +import "../../src/interfaces/IOperatorManager.sol"; + +interface IZkBobDirectDepositsAdmin is IZkBobDirectDepositQueue, IZkBobDirectDepositsETH { + function setOperatorManager(IOperatorManager _operatorManager) external; + + function setDirectDepositFee(uint64 _fee) external; + + function setDirectDepositTimeout(uint40 _timeout) external; +} diff --git a/test/interfaces/IZkBobPoolAdmin.sol b/test/interfaces/IZkBobPoolAdmin.sol new file mode 100644 index 0000000..950d614 --- /dev/null +++ b/test/interfaces/IZkBobPoolAdmin.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../src/zkbob/utils/ZkBobAccounting.sol"; +import "../../src/interfaces/ITokenSeller.sol"; +import "../../src/interfaces/IOperatorManager.sol"; + +interface IZkBobPoolAdmin { + function denominator() external pure returns (uint256); + + function pool_index() external view returns (uint256); + + function initialize( + uint256 _root, + uint256 _tvlCap, + uint256 _dailyDepositCap, + uint256 _dailyWithdrawalCap, + uint256 _dailyUserDepositCap, + uint256 _depositCap, + uint256 _dailyUserDirectDepositCap, + uint256 _directDepositCap + ) + external; + + function setTokenSeller(address _tokenSeller) external; + + function tokenSeller() external returns (ITokenSeller); + + function setOperatorManager(IOperatorManager _operatorManager) external; + + function transact() external; + + function appendDirectDeposits( + uint256 _root_after, + uint256[] calldata _indices, + uint256 _out_commit, + uint256[8] memory _batch_deposit_proof, + uint256[8] memory _tree_proof + ) + external; + + function recordDirectDeposit(address _sender, uint256 _amount) external; + + function withdrawFee(address _operator, address _to) external; + + function setLimits( + uint8 _tier, + uint256 _tvlCap, + uint256 _dailyDepositCap, + uint256 _dailyWithdrawalCap, + uint256 _dailyUserDepositCap, + uint256 _depositCap, + uint256 _dailyUserDirectDepositCap, + uint256 _directDepositCap + ) + external; + + function resetDailyLimits(uint8 _tier) external; + + function setUsersTier(uint8 _tier, address[] memory _users) external; + + function transfer_verifier() external view returns (address); + + function tree_verifier() external view returns (address); + + function batch_deposit_verifier() external view returns (address); + + function operatorManager() external view returns (address); + + function roots(uint256) external view returns (uint256); + + function all_messages_hash() external view returns (bytes32); + + function nullifiers(uint256) external view returns (uint256); + + function accumulatedFee(address) external view returns (uint256); + + function token() external view returns (address); + + function direct_deposit_queue() external view returns (address); + + function pool_id() external view returns (uint256); + + function _root() external view returns (uint256); + + function _pool_id() external view returns (uint256); + + function _withdrawNative(address user, uint256 tokenAmount) external returns (uint256); + + function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) external; + + function _isOwner() external view returns (bool); + + function _txCount() external view returns (uint256); + + function _setLimits( + uint8 _tier, + uint256 _tvlCap, + uint256 _dailyDepositCap, + uint256 _dailyWithdrawalCap, + uint256 _dailyUserDepositCap, + uint256 _depositCap, + uint256 _dailyUserDirectDepositCap, + uint256 _directDepositCap + ) + external; + + function _checkDepositLimits(address _sender, uint256 _amount) external view; + + function _checkWithdrawalLimits(address _receiver, uint256 _amount) external view; + + function _checkDirectDepositLimits(address _sender, uint256 _amount) external view; + + function _setUsersTier(uint8 _tier, address[] memory _users) external; + + function getLimitsFor(address _user) external view returns (ZkBobAccounting.Limits memory); + + function setKycProvidersManager(IKycProvidersManager _kycProvidersManager) external; +} diff --git a/test/zkbob/ZkBobPool.t.sol b/test/zkbob/ZkBobPool.t.sol new file mode 100644 index 0000000..abbc23f --- /dev/null +++ b/test/zkbob/ZkBobPool.t.sol @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import "../shared/Env.t.sol"; +import "../mocks/TransferVerifierMock.sol"; +import "../mocks/TreeUpdateVerifierMock.sol"; +import "../mocks/BatchDepositVerifierMock.sol"; +import "../mocks/DummyImpl.sol"; +import "../../src/proxy/EIP1967Proxy.sol"; +import "../../src/zkbob/ZkBobPoolETH.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; +import "../../src/BobToken.sol"; +import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../shared/ForkTests.t.sol"; +import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; +import "../interfaces/IZkBobDirectDepositsAdmin.sol"; +import "../interfaces/IZkBobPoolAdmin.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { + address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; + address constant uniV3Positions = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address constant usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant permit2Address = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + bytes constant zkAddress = "QsnTijXekjRm9hKcq5kLNPsa6P4HtMRrc3RxVx3jsLHeo2AiysYxVJP86mriHfN"; + + bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); + bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + event Message(uint256 indexed index, bytes32 indexed hash, bytes message); + + event SubmitDirectDeposit( + address indexed sender, + uint256 indexed nonce, + address fallbackUser, + ZkAddress.ZkAddress zkAddress, + uint64 deposit + ); + event RefundDirectDeposit(uint256 indexed nonce, address receiver, uint256 amount); + event CompleteDirectDepositBatch(uint256[] indices); + + IZkBobPoolAdmin pool; + IZkBobDirectDepositsAdmin queue; + address token; + IOperatorManager operatorManager; + + function testSimpleTransaction() public { + bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodeTransfer(); + _transact(data2); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user3), 0.02 ether); + } + + function testGetters() public { + assertEq(pool.pool_index(), 0); + assertEq(pool.denominator(), 1 gwei); + + bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data1); + + assertEq(pool.pool_index(), 128); + + bytes memory data2 = _encodeTransfer(); + _transact(data2); + + assertEq(pool.pool_index(), 256); + } + + function testUsersTiers() public { + pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 0, 0); + address[] memory users = new address[](1); + users[0] = user2; + pool.setUsersTier(1, users); + + assertEq(pool.getLimitsFor(user1).tier, 0); + assertEq(pool.getLimitsFor(user1).depositCap, 10_000 gwei); + assertEq(pool.getLimitsFor(user2).tier, 1); + assertEq(pool.getLimitsFor(user2).depositCap, 20_000 gwei); + } + + function testResetDailyLimits() public { + deal(token, user1, 10 ether); + + bytes memory data1 = _encodePermitDeposit(5 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodeWithdrawal(user1, 4 ether, 0 ether); + _transact(data2); + + assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 5 gwei); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 gwei); + + pool.resetDailyLimits(0); + + assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 0); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 0); + } + + function testSetOperatorManager() public { + assertEq(address(pool.operatorManager()), address(operatorManager)); + + IOperatorManager newOperatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); + pool.setOperatorManager(newOperatorManager); + + assertEq(address(pool.operatorManager()), address(newOperatorManager)); + } + + function testPermitDeposit() public { + bytes memory data = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user1), 0.49 ether); + assertEq(IERC20(token).balanceOf(address(pool)), 0.5 ether); + assertEq(IERC20(token).balanceOf(user3), 0.01 ether); + } + + function testUsualDeposit() public { + vm.prank(user1); + IERC20(token).approve(address(pool), 0.51 ether); + + bytes memory data = _encodeDeposit(0.5 ether, 0.01 ether); + _transact(data); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user1), 0.49 ether); + assertEq(IERC20(token).balanceOf(address(pool)), 0.5 ether); + assertEq(IERC20(token).balanceOf(user3), 0.01 ether); + } + + function testWithdrawal() public { + bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether, 0 ether); + _transact(data2); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user1), 0.59 ether); + assertEq(IERC20(token).balanceOf(address(pool)), 0.39 ether); + assertEq(IERC20(token).balanceOf(user3), 0.02 ether); + } + + function testRejectNegativeDeposits() public { + bytes memory data1 = _encodePermitDeposit(0.99 ether, 0.01 ether); + _transact(data1); + + bytes memory data2 = _encodePermitDeposit(-0.5 ether, 1 ether); + _transactReverted(data2, "ZkBobPool: incorrect deposit amounts"); + + vm.prank(user1); + IERC20(token).approve(address(pool), 0.5 ether); + + bytes memory data3 = _encodeDeposit(-0.5 ether, 1 ether); + _transactReverted(data3, "ZkBobPool: incorrect deposit amounts"); + } + + function _setUpDD() internal { + deal(user1, 100 ether); + deal(user2, 100 ether); + deal(address(token), user1, 100 ether); + deal(address(token), user2, 100 ether); + + pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 25 ether, 10 ether); + address[] memory users = new address[](1); + users[0] = user1; + pool.setUsersTier(1, users); + + queue.setDirectDepositFee(0.1 gwei); + } + + function testDirectDepositSubmit() public { + _setUpDD(); + + vm.prank(user2); + vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); + _transferAndCall(10 ether, user2, zkAddress); + + vm.startPrank(user1); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit amount is too low"); + _transferAndCall(0.01 ether, user2, zkAddress); + + vm.expectRevert(ZkAddress.InvalidZkAddressLength.selector); + _transferAndCall(10 ether, user2, "invalid"); + + vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); + _transferAndCall(15 ether, user2, zkAddress); + + vm.expectEmit(true, true, false, true); + emit SubmitDirectDeposit(user1, 0, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); + _transferAndCall(10 ether, user2, zkAddress); + + IERC20(token).approve(address(queue), 10 ether); + vm.expectEmit(true, true, false, true); + emit SubmitDirectDeposit(user1, 1, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); + queue.directDeposit(user2, 10 ether, zkAddress); + + vm.expectRevert("ZkBobAccounting: daily user direct deposit cap exceeded"); + _transferAndCall(10 ether, user2, zkAddress); + + for (uint256 i = 0; i < 2; i++) { + IZkBobDirectDeposits.DirectDeposit memory deposit = queue.getDirectDeposit(i); + assertEq(deposit.fallbackReceiver, user2); + assertEq(deposit.sent, 10 ether); + assertEq(deposit.deposit, 9.9 gwei); + assertEq(deposit.fee, 0.1 gwei); + assertEq(uint8(deposit.status), uint8(IZkBobDirectDeposits.DirectDepositStatus.Pending)); + } + vm.stopPrank(); + } + + function testAppendDirectDeposits() public { + _setUpDD(); + + vm.prank(user1); + _transferAndCall(10 ether, user2, zkAddress); + + vm.prank(user1); + _transferAndCall(5 ether, user2, zkAddress); + + uint256[] memory indices = new uint256[](2); + indices[0] = 0; + indices[1] = 1; + address verifier = address(pool.batch_deposit_verifier()); + uint256 outCommitment = _randFR(); + bytes memory data = abi.encodePacked( + outCommitment, + bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(9.9 gwei), // first deposit amount + bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(4.9 gwei), // second deposit amount + new bytes(14 * 50) + ); + vm.expectCall( + verifier, + abi.encodeWithSelector( + IBatchDepositVerifier.verifyProof.selector, + [ + uint256(keccak256(data)) % 21888242871839275222246405745257275088548364400416034343698204186575808495617 + ] + ) + ); + vm.expectEmit(true, false, false, true); + emit CompleteDirectDepositBatch(indices); + bytes memory message = abi.encodePacked( + bytes4(0x02000001), // uint16(2) in little endian ++ MESSAGE_PREFIX_DIRECT_DEPOSIT_V1 + uint64(0), // first deposit nonce + bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(9.9 gwei), // first deposit amount + uint64(1), // second deposit nonce + bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) + bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), + uint64(4.9 gwei) // second deposit amount + ); + vm.expectEmit(true, false, false, true); + emit Message(128, bytes32(0), message); + vm.prank(user2); + pool.appendDirectDeposits(_randFR(), indices, outCommitment, _randProof(), _randProof()); + } + + function testRefundDirectDeposit() public { + _setUpDD(); + + vm.prank(user1); + _transferAndCall(10 ether + 1, user2, zkAddress); + + vm.prank(user1); + _transferAndCall(5 ether + 1, user2, zkAddress); + + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); + queue.refundDirectDeposit(0); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); + queue.refundDirectDeposit(1); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); + queue.refundDirectDeposit(2); + + deal(address(token), user2, 0); + + vm.prank(user2); + vm.expectEmit(true, false, false, true); + emit RefundDirectDeposit(0, user2, 10 ether + 1); + queue.refundDirectDeposit(0); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); + queue.refundDirectDeposit(0); + assertEq(IERC20(token).balanceOf(user2), 10 ether + 1); + + skip(2 days); + + vm.expectEmit(true, false, false, true); + emit RefundDirectDeposit(1, user2, 5 ether + 1); + queue.refundDirectDeposit(1); + vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); + queue.refundDirectDeposit(1); + assertEq(IERC20(token).balanceOf(user2), 15 ether + 2); + } + + function testDepositForUserWithKYCPassed() public { + uint8 tier = 254; + ERC721PresetMinterPauserAutoId nft = new ERC721PresetMinterPauserAutoId("Test NFT", "tNFT", "http://nft.url/"); + + SimpleKYCProviderManager manager = new SimpleKYCProviderManager(nft, tier); + pool.setKycProvidersManager(manager); + + pool.setLimits(tier, 50 ether, 10 ether, 2 ether, 6 ether, 5 ether, 0, 0); + address[] memory users = new address[](1); + users[0] = user1; + pool.setUsersTier(tier, users); + + nft.mint(user1); + + deal(address(token), address(user1), 10 ether); + + bytes memory data = _encodePermitDeposit(4 ether, 0.01 ether); + _transact(data); + + bytes memory data2 = _encodeWithdrawal(user1, 1 ether, 0 ether); + _transact(data2); + + bytes memory data3 = _encodePermitDeposit(3 ether, 0.01 ether); + _transactReverted(data3, "ZkBobAccounting: daily user deposit cap exceeded"); + + bytes memory data4 = _encodeWithdrawal(user1, 2 ether, 0 ether); + _transactReverted(data4, "ZkBobAccounting: daily withdrawal cap exceeded"); + + assertEq(pool.getLimitsFor(user1).dailyUserDepositCapUsage, 4 gwei); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 1.01 gwei); // 1 requested + 0.01 fees + } + + function _encodeDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { + bytes32 nullifier = bytes32(_randFR()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, ECDSA.toEthSignedMessageHash(nullifier)); + bytes memory data = abi.encodePacked( + ZkBobPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + data = abi.encodePacked(data, uint16(0), uint16(44), uint64(_fee / 1 gwei), bytes4(0x01000000), _randFR()); + return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); + } + + function _encodeWithdrawal(address _to, uint256 _amount, uint256 _nativeAmount) internal returns (bytes memory) { + bytes memory data = abi.encodePacked( + ZkBobPool.transact.selector, + _randFR(), + _randFR(), + uint48(0), + uint112(0), + int64(-int256((_amount + 0.01 ether) / 1 gwei)) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + return abi.encodePacked( + data, + uint16(2), + uint16(72), + uint64(0.01 ether / 1 gwei), + uint64(_nativeAmount / 1 gwei), + _to, + bytes4(0x01000000), + _randFR() + ); + } + + function _encodeTransfer() internal returns (bytes memory) { + bytes memory data = abi.encodePacked( + ZkBobPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), int64(-0.01 ether / 1 gwei) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + return abi.encodePacked(data, uint16(1), uint16(44), uint64(0.01 ether / 1 gwei), bytes4(0x01000000), _randFR()); + } + + function _transact(bytes memory _data) internal { + vm.prank(user2); + (bool status,) = address(pool).call(_data); + require(status, "transact() reverted"); + } + + function _transactReverted(bytes memory _data, bytes memory _revertReason) internal { + vm.prank(user2); + (bool status, bytes memory returnData) = address(pool).call(_data); + assert(!status); + assertEq(returnData, abi.encodeWithSignature("Error(string)", _revertReason)); + } + + function _randFR() internal returns (uint256) { + return uint256(keccak256(abi.encode(gasleft()))) + % 21888242871839275222246405745257275088696311157297823662689037894645226208583; + } + + function _randProof() internal returns (uint256[8] memory) { + return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; + } + + function _encodePermitDeposit(int256 _amount, uint256 _fee) internal virtual returns (bytes memory); + + function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal virtual; +} diff --git a/test/zkbob/ZkBobPoolERC20.t.sol b/test/zkbob/ZkBobPoolERC20.t.sol index e8af240..cd1a28d 100644 --- a/test/zkbob/ZkBobPoolERC20.t.sol +++ b/test/zkbob/ZkBobPoolERC20.t.sol @@ -20,33 +20,11 @@ import "../../src/zkbob/manager/MutableOperatorManager.sol"; import "../../src/utils/UniswapV3Seller.sol"; import "../shared/ForkTests.t.sol"; import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; +import "./ZkBobPool.t.sol"; -contract ZkBobPoolERC20Test is AbstractMainnetForkTest { +contract ZkBobPoolERC20Test is AbstractZkBobPoolTest { uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; - - address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; - address constant uniV3Positions = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; - address constant usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - - bytes constant zkAddress = "QsnTijXekjRm9hKcq5kLNPsa6P4HtMRrc3RxVx3jsLHeo2AiysYxVJP86mriHfN"; - - ZkBobPoolERC20 pool; - ZkBobDirectDepositQueue queue; BobToken bob; - IOperatorManager operatorManager; - - event Message(uint256 indexed index, bytes32 indexed hash, bytes message); - - event SubmitDirectDeposit( - address indexed sender, - uint256 indexed nonce, - address fallbackUser, - ZkAddress.ZkAddress zkAddress, - uint64 deposit - ); - event RefundDirectDeposit(uint256 indexed nonce, address receiver, uint256 amount); - event CompleteDirectDepositBatch(uint256[] indices); function setUp() public { EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, ""); @@ -73,11 +51,11 @@ contract ZkBobPoolERC20Test is AbstractMainnetForkTest { 0 ); poolProxy.upgradeToAndCall(address(impl), initData); - pool = ZkBobPoolERC20(address(poolProxy)); + pool = IZkBobPoolAdmin(address(poolProxy)); ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(bob)); queueProxy.upgradeTo(address(queueImpl)); - queue = ZkBobDirectDepositQueue(address(queueProxy)); + queue = IZkBobDirectDepositsAdmin(address(queueProxy)); operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); pool.setOperatorManager(operatorManager); @@ -86,33 +64,7 @@ contract ZkBobPoolERC20Test is AbstractMainnetForkTest { queue.setDirectDepositTimeout(1 days); bob.mint(address(user1), 1 ether); - } - - function testSimpleTransaction() public { - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodeTransfer(); - _transact(data2); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(bob.balanceOf(user3), 0.02 ether); - } - - function testGetters() public { - assertEq(pool.pool_index(), 0); - assertEq(pool.denominator(), 1 gwei); - - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data1); - - assertEq(pool.pool_index(), 128); - - bytes memory data2 = _encodeTransfer(); - _transact(data2); - - assertEq(pool.pool_index(), 256); + token = address(bob); } function testAuthRights() public { @@ -134,84 +86,6 @@ contract ZkBobPoolERC20Test is AbstractMainnetForkTest { vm.stopPrank(); } - function testUsersTiers() public { - pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 0, 0); - address[] memory users = new address[](1); - users[0] = user2; - pool.setUsersTier(1, users); - - assertEq(pool.getLimitsFor(user1).tier, 0); - assertEq(pool.getLimitsFor(user1).depositCap, 10_000 gwei); - assertEq(pool.getLimitsFor(user2).tier, 1); - assertEq(pool.getLimitsFor(user2).depositCap, 20_000 gwei); - } - - function testResetDailyLimits() public { - bob.mint(address(user1), 10 ether); - - bytes memory data1 = _encodePermitDeposit(5 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodeWithdrawal(user1, 4 ether, 0 ether); - _transact(data2); - - assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 5 gwei); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 gwei); - - pool.resetDailyLimits(0); - - assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 0); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 0); - } - - function testSetOperatorManager() public { - assertEq(address(pool.operatorManager()), address(operatorManager)); - - IOperatorManager newOperatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); - pool.setOperatorManager(newOperatorManager); - - assertEq(address(pool.operatorManager()), address(newOperatorManager)); - } - - function testPermitDeposit() public { - bytes memory data = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(bob.balanceOf(user1), 0.49 ether); - assertEq(bob.balanceOf(address(pool)), 0.5 ether); - assertEq(bob.balanceOf(user3), 0.01 ether); - } - - function testUsualDeposit() public { - vm.prank(user1); - bob.approve(address(pool), 0.51 ether); - - bytes memory data = _encodeDeposit(0.5 ether, 0.01 ether); - _transact(data); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(bob.balanceOf(user1), 0.49 ether); - assertEq(bob.balanceOf(address(pool)), 0.5 ether); - assertEq(bob.balanceOf(user3), 0.01 ether); - } - - function testWithdrawal() public { - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether, 0 ether); - _transact(data2); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(bob.balanceOf(user1), 0.59 ether); - assertEq(bob.balanceOf(address(pool)), 0.39 ether); - assertEq(bob.balanceOf(user3), 0.02 ether); - } - function _setupNativeSwaps() internal { vm.makePersistent(address(pool), address(bob)); vm.makePersistent( @@ -324,193 +198,11 @@ contract ZkBobPoolERC20Test is AbstractMainnetForkTest { assertEq(user1.balance, quote2 + quote31); } - function testRejectNegativeDeposits() public { - bytes memory data1 = _encodePermitDeposit(0.99 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodePermitDeposit(-0.5 ether, 1 ether); - _transactReverted(data2, "ZkBobPool: incorrect deposit amounts"); - - vm.prank(user1); - bob.approve(address(pool), 0.5 ether); - - bytes memory data3 = _encodeDeposit(-0.5 ether, 1 ether); - _transactReverted(data3, "ZkBobPool: incorrect deposit amounts"); - } - - function _setUpDD() internal { - deal(address(bob), user1, 100 ether); - deal(address(bob), user2, 100 ether); - - pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 25 ether, 10 ether); - address[] memory users = new address[](1); - users[0] = user1; - pool.setUsersTier(1, users); - - queue.setDirectDepositFee(0.1 gwei); + function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal override { + bob.transferAndCall(address(queue), amount, abi.encode(fallbackUser, _zkAddress)); } - function testDirectDepositSubmit() public { - _setUpDD(); - - vm.prank(user2); - vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); - bob.transferAndCall(address(queue), 10 ether, abi.encode(user2, zkAddress)); - - vm.startPrank(user1); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit amount is too low"); - bob.transferAndCall(address(queue), 0.01 ether, abi.encode(user2, zkAddress)); - - vm.expectRevert(ZkAddress.InvalidZkAddressLength.selector); - bob.transferAndCall(address(queue), 10 ether, abi.encode(user2, "invalid")); - - vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); - bob.transferAndCall(address(queue), 15 ether, abi.encode(user2, zkAddress)); - - vm.expectEmit(true, true, false, true); - emit SubmitDirectDeposit(user1, 0, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); - bob.transferAndCall(address(queue), 10 ether, abi.encode(user2, zkAddress)); - - bob.approve(address(queue), 10 ether); - vm.expectEmit(true, true, false, true); - emit SubmitDirectDeposit(user1, 1, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); - queue.directDeposit(user2, 10 ether, zkAddress); - - vm.expectRevert("ZkBobAccounting: daily user direct deposit cap exceeded"); - bob.transferAndCall(address(queue), 10 ether, abi.encode(user2, zkAddress)); - - for (uint256 i = 0; i < 2; i++) { - IZkBobDirectDeposits.DirectDeposit memory deposit = queue.getDirectDeposit(i); - assertEq(deposit.fallbackReceiver, user2); - assertEq(deposit.sent, 10 ether); - assertEq(deposit.deposit, 9.9 gwei); - assertEq(deposit.fee, 0.1 gwei); - assertEq(uint8(deposit.status), uint8(IZkBobDirectDeposits.DirectDepositStatus.Pending)); - } - vm.stopPrank(); - } - - function testAppendDirectDeposits() public { - _setUpDD(); - - vm.prank(user1); - bob.transferAndCall(address(queue), 10 ether, abi.encode(user2, zkAddress)); - - vm.prank(user1); - bob.transferAndCall(address(queue), 5 ether, abi.encode(user2, zkAddress)); - - uint256[] memory indices = new uint256[](2); - indices[0] = 0; - indices[1] = 1; - address verifier = address(pool.batch_deposit_verifier()); - uint256 outCommitment = _randFR(); - bytes memory data = abi.encodePacked( - outCommitment, - bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(9.9 gwei), // first deposit amount - bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(4.9 gwei), // second deposit amount - new bytes(14 * 50) - ); - vm.expectCall( - verifier, - abi.encodeWithSelector( - IBatchDepositVerifier.verifyProof.selector, - [ - uint256(keccak256(data)) % 21888242871839275222246405745257275088548364400416034343698204186575808495617 - ] - ) - ); - vm.expectEmit(true, false, false, true); - emit CompleteDirectDepositBatch(indices); - bytes memory message = abi.encodePacked( - bytes4(0x02000001), // uint16(2) in little endian ++ MESSAGE_PREFIX_DIRECT_DEPOSIT_V1 - uint64(0), // first deposit nonce - bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(9.9 gwei), // first deposit amount - uint64(1), // second deposit nonce - bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(4.9 gwei) // second deposit amount - ); - vm.expectEmit(true, false, false, true); - emit Message(128, bytes32(0), message); - vm.prank(user2); - pool.appendDirectDeposits(_randFR(), indices, outCommitment, _randProof(), _randProof()); - } - - function testRefundDirectDeposit() public { - _setUpDD(); - - vm.prank(user1); - bob.transferAndCall(address(queue), 10 ether + 1, abi.encode(user2, zkAddress)); - - vm.prank(user1); - bob.transferAndCall(address(queue), 5 ether + 1, abi.encode(user2, zkAddress)); - - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); - queue.refundDirectDeposit(0); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); - queue.refundDirectDeposit(1); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); - queue.refundDirectDeposit(2); - - deal(address(bob), user2, 0); - - vm.prank(user2); - vm.expectEmit(true, false, false, true); - emit RefundDirectDeposit(0, user2, 10 ether + 1); - queue.refundDirectDeposit(0); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); - queue.refundDirectDeposit(0); - assertEq(bob.balanceOf(user2), 10 ether + 1); - - skip(2 days); - - vm.expectEmit(true, false, false, true); - emit RefundDirectDeposit(1, user2, 5 ether + 1); - queue.refundDirectDeposit(1); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); - queue.refundDirectDeposit(1); - assertEq(bob.balanceOf(user2), 15 ether + 2); - } - - function testDepositForUserWithKYCPassed() public { - uint8 tier = 254; - ERC721PresetMinterPauserAutoId nft = new ERC721PresetMinterPauserAutoId("Test NFT", "tNFT", "http://nft.url/"); - - SimpleKYCProviderManager manager = new SimpleKYCProviderManager(nft, tier); - pool.setKycProvidersManager(manager); - - pool.setLimits(tier, 50 ether, 10 ether, 2 ether, 6 ether, 5 ether, 0, 0); - address[] memory users = new address[](1); - users[0] = user1; - pool.setUsersTier(tier, users); - - nft.mint(user1); - - bob.mint(address(user1), 10 ether); - - bytes memory data = _encodePermitDeposit(4 ether, 0.01 ether); - _transact(data); - - bytes memory data2 = _encodeWithdrawal(user1, 1 ether, 0 ether); - _transact(data2); - - bytes memory data3 = _encodePermitDeposit(3 ether, 0.01 ether); - _transactReverted(data3, "ZkBobAccounting: daily user deposit cap exceeded"); - - bytes memory data4 = _encodeWithdrawal(user1, 2 ether, 0 ether); - _transactReverted(data4, "ZkBobAccounting: daily withdrawal cap exceeded"); - - assertEq(pool.getLimitsFor(user1).dailyUserDepositCapUsage, 4 gwei); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 1.01 gwei); // 1 requested + 0.01 fees - } - - function _encodePermitDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { + function _encodePermitDeposit(int256 _amount, uint256 _fee) internal override returns (bytes memory) { uint256 expiry = block.timestamp + 1 hours; bytes32 nullifier = bytes32(_randFR()); (uint8 v, bytes32 r, bytes32 s) = _signSaltedPermit( @@ -528,66 +220,6 @@ contract ZkBobPoolERC20Test is AbstractMainnetForkTest { return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); } - function _encodeDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { - bytes32 nullifier = bytes32(_randFR()); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, ECDSA.toEthSignedMessageHash(nullifier)); - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - data = abi.encodePacked(data, uint16(0), uint16(44), uint64(_fee / 1 gwei), bytes4(0x01000000), _randFR()); - return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); - } - - function _encodeWithdrawal(address _to, uint256 _amount, uint256 _nativeAmount) internal returns (bytes memory) { - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, - _randFR(), - _randFR(), - uint48(0), - uint112(0), - int64(-int256((_amount + 0.01 ether) / 1 gwei)) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - return abi.encodePacked( - data, - uint16(2), - uint16(72), - uint64(0.01 ether / 1 gwei), - uint64(_nativeAmount / 1 gwei), - _to, - bytes4(0x01000000), - _randFR() - ); - } - - function _encodeTransfer() internal returns (bytes memory) { - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), int64(-0.01 ether / 1 gwei) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - return abi.encodePacked(data, uint16(1), uint16(44), uint64(0.01 ether / 1 gwei), bytes4(0x01000000), _randFR()); - } - - function _transact(bytes memory _data) internal { - vm.prank(user2); - (bool status,) = address(pool).call(_data); - require(status, "transact() reverted"); - } - - function _transactReverted(bytes memory _data, bytes memory _revertReason) internal { - vm.prank(user2); - (bool status, bytes memory returnData) = address(pool).call(_data); - assert(!status); - assertEq(returnData, abi.encodeWithSignature("Error(string)", _revertReason)); - } - function _signSaltedPermit( uint256 _pk, address _holder, @@ -606,13 +238,4 @@ contract ZkBobPoolERC20Test is AbstractMainnetForkTest { ); return vm.sign(_pk, digest); } - - function _randFR() private returns (uint256) { - return uint256(keccak256(abi.encode(gasleft()))) - % 21888242871839275222246405745257275088696311157297823662689037894645226208583; - } - - function _randProof() private returns (uint256[8] memory) { - return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; - } } diff --git a/test/zkbob/ZkBobPoolETH.t.sol b/test/zkbob/ZkBobPoolETH.t.sol index 569ab71..44b30f2 100644 --- a/test/zkbob/ZkBobPoolETH.t.sol +++ b/test/zkbob/ZkBobPoolETH.t.sol @@ -20,45 +20,16 @@ import "../shared/ForkTests.t.sol"; import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; import "../../src/zkbob/manager/kyc/SimpleKYCProviderManager.sol"; import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; +import "./ZkBobPool.t.sol"; -contract ZkBobPoolETHTest is AbstractMainnetForkTest { +contract ZkBobPoolETHTest is AbstractZkBobPoolTest { uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; - address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; - address constant uniV3Positions = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; - address constant usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address constant permit2Address = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - - bytes constant zkAddress = "QsnTijXekjRm9hKcq5kLNPsa6P4HtMRrc3RxVx3jsLHeo2AiysYxVJP86mriHfN"; - - bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); - bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( - "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" - ); - - ZkBobPoolETH pool; - ZkBobDirectDepositQueueETH queue; - IWETH9 token; - IOperatorManager operatorManager; IPermit2 permit2; - event Message(uint256 indexed index, bytes32 indexed hash, bytes message); - - event SubmitDirectDeposit( - address indexed sender, - uint256 indexed nonce, - address fallbackUser, - ZkAddress.ZkAddress zkAddress, - uint64 deposit - ); - event RefundDirectDeposit(uint256 indexed nonce, address receiver, uint256 amount); - event CompleteDirectDepositBatch(uint256[] indices); - function setUp() public { vm.createSelectFork(forkRpcUrl, forkBlock); - token = IWETH9(weth); + token = weth; permit2 = IPermit2(permit2Address); EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); @@ -80,11 +51,11 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { 0 ); poolProxy.upgradeToAndCall(address(impl), initData); - pool = ZkBobPoolETH(payable(address(poolProxy))); + pool = IZkBobPoolAdmin(payable(address(poolProxy))); ZkBobDirectDepositQueueETH queueImpl = new ZkBobDirectDepositQueueETH(address(pool), address(token)); queueProxy.upgradeTo(address(queueImpl)); - queue = ZkBobDirectDepositQueueETH(address(queueProxy)); + queue = IZkBobDirectDepositsAdmin(address(queueProxy)); operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); pool.setOperatorManager(operatorManager); @@ -95,48 +66,21 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { deal(weth, user1, 1 ether); vm.startPrank(user1); - token.approve(permit2Address, type(uint256).max); - token.approve(address(queue), type(uint256).max); + IERC20(token).approve(permit2Address, type(uint256).max); + IERC20(token).approve(address(queue), type(uint256).max); vm.stopPrank(); vm.startPrank(user2); - token.approve(permit2Address, type(uint256).max); - token.approve(address(queue), type(uint256).max); + IERC20(token).approve(permit2Address, type(uint256).max); + IERC20(token).approve(address(queue), type(uint256).max); vm.stopPrank(); vm.startPrank(user3); - token.approve(permit2Address, type(uint256).max); - token.approve(address(queue), type(uint256).max); + IERC20(token).approve(permit2Address, type(uint256).max); + IERC20(token).approve(address(queue), type(uint256).max); vm.stopPrank(); } - function testSimpleTransaction() public { - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodeTransfer(); - _transact(data2); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(token.balanceOf(user3), 0.02 ether); - } - - function testGetters() public { - assertEq(pool.pool_index(), 0); - assertEq(pool.denominator(), 1 gwei); - - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data1); - - assertEq(pool.pool_index(), 128); - - bytes memory data2 = _encodeTransfer(); - _transact(data2); - - assertEq(pool.pool_index(), 256); - } - function testAuthRights() public { vm.startPrank(user1); @@ -154,84 +98,6 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { vm.stopPrank(); } - function testUsersTiers() public { - pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 0, 0); - address[] memory users = new address[](1); - users[0] = user2; - pool.setUsersTier(1, users); - - assertEq(pool.getLimitsFor(user1).tier, 0); - assertEq(pool.getLimitsFor(user1).depositCap, 10_000 gwei); - assertEq(pool.getLimitsFor(user2).tier, 1); - assertEq(pool.getLimitsFor(user2).depositCap, 20_000 gwei); - } - - function testResetDailyLimits() public { - deal(weth, user1, 10 ether); - - bytes memory data1 = _encodePermitDeposit(5 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodeWithdrawal(user1, 4 ether, 0 ether); - _transact(data2); - - assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 5 gwei); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 gwei); - - pool.resetDailyLimits(0); - - assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 0); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 0); - } - - function testSetOperatorManager() public { - assertEq(address(pool.operatorManager()), address(operatorManager)); - - IOperatorManager newOperatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); - pool.setOperatorManager(newOperatorManager); - - assertEq(address(pool.operatorManager()), address(newOperatorManager)); - } - - function testPermitDeposit() public { - bytes memory data = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(token.balanceOf(user1), 0.49 ether); - assertEq(token.balanceOf(address(pool)), 0.5 ether); - assertEq(token.balanceOf(user3), 0.01 ether); - } - - function testUsualDeposit() public { - vm.prank(user1); - token.approve(address(pool), 0.51 ether); - - bytes memory data = _encodeDeposit(0.5 ether, 0.01 ether); - _transact(data); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(token.balanceOf(user1), 0.49 ether); - assertEq(token.balanceOf(address(pool)), 0.5 ether); - assertEq(token.balanceOf(user3), 0.01 ether); - } - - function testWithdrawal() public { - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether, 0 ether); - _transact(data2); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(token.balanceOf(user1), 0.59 ether); - assertEq(token.balanceOf(address(pool)), 0.39 ether); - assertEq(token.balanceOf(user3), 0.02 ether); - } - function testNativeWithdrawal() public { vm.deal(user1, 0); @@ -250,207 +116,25 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { vm.prank(user3); pool.withdrawFee(user2, user3); - assertEq(token.balanceOf(user1), 0.1 ether); - assertEq(token.balanceOf(dummy), 0.1 ether); - assertEq(token.balanceOf(address(pool)), 0.17 ether); - assertEq(token.balanceOf(user3), 0.03 ether); + assertEq(IERC20(token).balanceOf(user1), 0.1 ether); + assertEq(IERC20(token).balanceOf(dummy), 0.1 ether); + assertEq(IERC20(token).balanceOf(address(pool)), 0.17 ether); + assertEq(IERC20(token).balanceOf(user3), 0.03 ether); assertGt(user1.balance, 1 gwei); assertEq(user1.balance, quote2); assertGt(dummy.balance, 1 gwei); assertEq(dummy.balance, quote3); } - function testRejectNegativeDeposits() public { - bytes memory data1 = _encodePermitDeposit(0.99 ether, 0.01 ether); - _transact(data1); - - bytes memory data2 = _encodePermitDeposit(-0.5 ether, 1 ether); - _transactReverted(data2, "ZkBobPool: incorrect deposit amounts"); - - vm.prank(user1); - token.approve(address(pool), 0.5 ether); - - bytes memory data3 = _encodeDeposit(-0.5 ether, 1 ether); - _transactReverted(data3, "ZkBobPool: incorrect deposit amounts"); - } - - function _setUpDD() internal { - deal(user1, 100 ether); - deal(address(token), user1, 100 ether); - deal(address(token), user2, 100 ether); - - pool.setLimits(1, 2_000_000 ether, 200_000 ether, 200_000 ether, 20_000 ether, 20_000 ether, 25 ether, 10 ether); - address[] memory users = new address[](1); - users[0] = user1; - pool.setUsersTier(1, users); - - queue.setDirectDepositFee(0.1 gwei); - } - - function testDirectDepositSubmit() public { - _setUpDD(); - - vm.prank(user2); - vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); - queue.directDeposit(user2, 10 ether, zkAddress); - - vm.startPrank(user1); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit amount is too low"); - queue.directDeposit(user2, 0.01 ether, zkAddress); - - vm.expectRevert(ZkAddress.InvalidZkAddressLength.selector); - queue.directDeposit(user2, 10 ether, bytes("invalid")); - - vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); - queue.directDeposit(user2, 15 ether, zkAddress); - - vm.expectEmit(true, true, false, true); - emit SubmitDirectDeposit(user1, 0, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); - queue.directDeposit(user2, 10 ether, zkAddress); - - vm.expectEmit(true, true, false, true); - emit SubmitDirectDeposit(user1, 1, user2, ZkAddress.parseZkAddress(zkAddress, 0), 9.9 gwei); - queue.directDeposit(user2, 10 ether, zkAddress); - - vm.expectRevert("ZkBobAccounting: daily user direct deposit cap exceeded"); - queue.directDeposit(user2, 10 ether, zkAddress); - - for (uint256 i = 0; i < 2; i++) { - IZkBobDirectDeposits.DirectDeposit memory deposit = queue.getDirectDeposit(i); - assertEq(deposit.fallbackReceiver, user2); - assertEq(deposit.sent, 10 ether); - assertEq(deposit.deposit, 9.9 gwei); - assertEq(deposit.fee, 0.1 gwei); - assertEq(uint8(deposit.status), uint8(IZkBobDirectDeposits.DirectDepositStatus.Pending)); - } - vm.stopPrank(); - } - - function testAppendDirectDeposits() public { - _setUpDD(); - - vm.prank(user1); - queue.directNativeDeposit{value: 10 ether}(user2, zkAddress); - - vm.prank(user1); - queue.directNativeDeposit{value: 5 ether}(user2, zkAddress); - - uint256[] memory indices = new uint256[](2); - indices[0] = 0; - indices[1] = 1; - address verifier = address(pool.batch_deposit_verifier()); - uint256 outCommitment = _randFR(); - bytes memory data = abi.encodePacked( - outCommitment, - bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(9.9 gwei), // first deposit amount - bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(4.9 gwei), // second deposit amount - new bytes(14 * 50) - ); - vm.expectCall( - verifier, - abi.encodeWithSelector( - IBatchDepositVerifier.verifyProof.selector, - [ - uint256(keccak256(data)) % 21888242871839275222246405745257275088548364400416034343698204186575808495617 - ] - ) - ); - vm.expectEmit(true, false, false, true); - emit CompleteDirectDepositBatch(indices); - bytes memory message = abi.encodePacked( - bytes4(0x02000001), // uint16(2) in little endian ++ MESSAGE_PREFIX_DIRECT_DEPOSIT_V1 - uint64(0), // first deposit nonce - bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(9.9 gwei), // first deposit amount - uint64(1), // second deposit nonce - bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) - bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(4.9 gwei) // second deposit amount - ); - vm.expectEmit(true, false, false, true); - emit Message(128, bytes32(0), message); - vm.prank(user2); - pool.appendDirectDeposits(_randFR(), indices, outCommitment, _randProof(), _randProof()); - } - - function testRefundDirectDeposit() public { - _setUpDD(); - - vm.prank(user1); - queue.directNativeDeposit{value: 10 ether + 1}(user2, zkAddress); - - vm.prank(user1); - queue.directNativeDeposit{value: 5 ether + 1}(user2, zkAddress); - - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); - queue.refundDirectDeposit(0); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); - queue.refundDirectDeposit(1); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); - queue.refundDirectDeposit(2); - - deal(address(token), user2, 0); - - vm.prank(user2); - vm.expectEmit(true, false, false, true); - emit RefundDirectDeposit(0, user2, 10 ether + 1); - queue.refundDirectDeposit(0); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); - queue.refundDirectDeposit(0); - assertEq(token.balanceOf(user2), 10 ether + 1); - - skip(2 days); - - vm.expectEmit(true, false, false, true); - emit RefundDirectDeposit(1, user2, 5 ether + 1); - queue.refundDirectDeposit(1); - vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); - queue.refundDirectDeposit(1); - assertEq(token.balanceOf(user2), 15 ether + 2); - } - - function testDepositForUserWithKYCPassed() public { - uint8 tier = 254; - ERC721PresetMinterPauserAutoId nft = new ERC721PresetMinterPauserAutoId("Test NFT", "tNFT", "http://nft.url/"); - - SimpleKYCProviderManager manager = new SimpleKYCProviderManager(nft, tier); - pool.setKycProvidersManager(manager); - - pool.setLimits(tier, 50 ether, 10 ether, 2 ether, 6 ether, 5 ether, 0, 0); - address[] memory users = new address[](1); - users[0] = user1; - pool.setUsersTier(tier, users); - - nft.mint(user1); - - deal(weth, address(user1), 10 ether); - - bytes memory data = _encodePermitDeposit(4 ether, 0.01 ether); - _transact(data); - - bytes memory data2 = _encodeWithdrawal(user1, 1 ether, 0 ether); - _transact(data2); - - bytes memory data3 = _encodePermitDeposit(3 ether, 0.01 ether); - _transactReverted(data3, "ZkBobAccounting: daily user deposit cap exceeded"); - - bytes memory data4 = _encodeWithdrawal(user1, 2 ether, 0 ether); - _transactReverted(data4, "ZkBobAccounting: daily withdrawal cap exceeded"); - - assertEq(pool.getLimitsFor(user1).dailyUserDepositCapUsage, 4 gwei); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 1.01 gwei); // 1 requested + 0.01 fees + function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal override { + queue.directNativeDeposit{value: amount}(fallbackUser, _zkAddress); } - function _encodePermitDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { + function _encodePermitDeposit(int256 _amount, uint256 _fee) internal override returns (bytes memory) { uint256 expiry = block.timestamp + 1 hours; bytes32 nullifier = bytes32(_randFR()); (uint8 v, bytes32 r, bytes32 s) = - _signSaltedPermit(pk1, user1, address(pool), uint256(_amount + int256(_fee)), expiry, nullifier); + _signSaltedPermit(pk1, user1, address(pool), uint256(_amount + int256(_fee)), 0, expiry, nullifier); bytes memory data = abi.encodePacked( ZkBobPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) ); @@ -463,66 +147,6 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); } - function _encodeDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { - bytes32 nullifier = bytes32(_randFR()); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, ECDSA.toEthSignedMessageHash(nullifier)); - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / 1 gwei) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - data = abi.encodePacked(data, uint16(0), uint16(44), uint64(_fee / 1 gwei), bytes4(0x01000000), _randFR()); - return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); - } - - function _encodeWithdrawal(address _to, uint256 _amount, uint256 _nativeAmount) internal returns (bytes memory) { - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, - _randFR(), - _randFR(), - uint48(0), - uint112(0), - int64(-int256((_amount + 0.01 ether) / 1 gwei)) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - return abi.encodePacked( - data, - uint16(2), - uint16(72), - uint64(0.01 ether / 1 gwei), - uint64(_nativeAmount / 1 gwei), - _to, - bytes4(0x01000000), - _randFR() - ); - } - - function _encodeTransfer() internal returns (bytes memory) { - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), int64(-0.01 ether / 1 gwei) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - return abi.encodePacked(data, uint16(1), uint16(44), uint64(0.01 ether / 1 gwei), bytes4(0x01000000), _randFR()); - } - - function _transact(bytes memory _data) internal { - vm.prank(user2); - (bool status,) = address(pool).call(_data); - require(status, "transact() reverted"); - } - - function _transactReverted(bytes memory _data, bytes memory _revertReason) internal { - vm.prank(user2); - (bool status, bytes memory returnData) = address(pool).call(_data); - assert(!status); - assertEq(returnData, abi.encodeWithSignature("Error(string)", _revertReason)); - } - function _getEIP712Hash( IPermit2.PermitTransferFrom memory permit, address spender @@ -555,6 +179,7 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { address _holder, address _spender, uint256 _value, + uint256 _nonce, uint256 _expiry, bytes32 _salt ) @@ -568,13 +193,4 @@ contract ZkBobPoolETHTest is AbstractMainnetForkTest { }); return vm.sign(_pk, _getEIP712Hash(permit, _spender)); } - - function _randFR() private returns (uint256) { - return uint256(keccak256(abi.encode(gasleft()))) - % 21888242871839275222246405745257275088696311157297823662689037894645226208583; - } - - function _randProof() private returns (uint256[8] memory) { - return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; - } } From 71179684c6c1008dd59ddb7b075fb3373337e1d9 Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Tue, 4 Apr 2023 14:52:18 +0300 Subject: [PATCH 07/11] added test --- test/zkbob/ZkBobPoolETH.t.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/zkbob/ZkBobPoolETH.t.sol b/test/zkbob/ZkBobPoolETH.t.sol index 44b30f2..4c05eff 100644 --- a/test/zkbob/ZkBobPoolETH.t.sol +++ b/test/zkbob/ZkBobPoolETH.t.sol @@ -81,6 +81,21 @@ contract ZkBobPoolETHTest is AbstractZkBobPoolTest { vm.stopPrank(); } + function testSimpleTransactionPermit2() public { + bytes memory data2 = _encodePermitDeposit(0.3 ether, 0.003 ether); + bytes memory data1 = _encodePermitDeposit(0.2 ether, 0.007 ether); + + _transact(data1); + _transact(data2); + + bytes memory data3 = _encodeTransfer(); + _transact(data3); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user3), 0.02 ether); + } + function testAuthRights() public { vm.startPrank(user1); From dc583253c7ab02063ccdf9fbc09e663f0cffe59c Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Tue, 4 Apr 2023 15:31:13 +0300 Subject: [PATCH 08/11] sending weth instead of sacrifice --- src/zkbob/ZkBobPoolETH.sol | 3 ++- test/zkbob/ZkBobPoolETH.t.sol | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/zkbob/ZkBobPoolETH.sol b/src/zkbob/ZkBobPoolETH.sol index 3d6bcec..3658618 100644 --- a/src/zkbob/ZkBobPoolETH.sol +++ b/src/zkbob/ZkBobPoolETH.sol @@ -52,7 +52,8 @@ contract ZkBobPoolETH is ZkBobPool { function _withdrawNative(address user, uint256 tokenAmount) internal override returns (uint256 spentAmount) { IWETH9(token).withdraw(tokenAmount); if (!payable(user).send(tokenAmount)) { - new Sacrifice{value: tokenAmount}(user); + IWETH9(token).deposit{value: tokenAmount}(); + IWETH9(token).transfer(user, tokenAmount); } return tokenAmount; } diff --git a/test/zkbob/ZkBobPoolETH.t.sol b/test/zkbob/ZkBobPoolETH.t.sol index 4c05eff..20863b1 100644 --- a/test/zkbob/ZkBobPoolETH.t.sol +++ b/test/zkbob/ZkBobPoolETH.t.sol @@ -35,7 +35,6 @@ contract ZkBobPoolETHTest is AbstractZkBobPoolTest { EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); - console2.log(weth, address(token)); ZkBobPoolETH impl = new ZkBobPoolETH(0, address(token), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy), permit2Address); @@ -132,13 +131,12 @@ contract ZkBobPoolETHTest is AbstractZkBobPoolTest { vm.prank(user3); pool.withdrawFee(user2, user3); assertEq(IERC20(token).balanceOf(user1), 0.1 ether); - assertEq(IERC20(token).balanceOf(dummy), 0.1 ether); + assertEq(IERC20(token).balanceOf(dummy), 0.1 ether + quote3); assertEq(IERC20(token).balanceOf(address(pool)), 0.17 ether); assertEq(IERC20(token).balanceOf(user3), 0.03 ether); assertGt(user1.balance, 1 gwei); assertEq(user1.balance, quote2); - assertGt(dummy.balance, 1 gwei); - assertEq(dummy.balance, quote3); + assertEq(dummy.balance, 0); } function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal override { From fe721d9a9ab65bd1ec54af9a65c9b85497712b8d Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Thu, 6 Apr 2023 09:44:01 +0300 Subject: [PATCH 09/11] Update src/zkbob/ZkBobPool.sol Co-authored-by: Kirill Fedoseev --- src/zkbob/ZkBobPool.sol | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index 5ae410e..a53a844 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -159,22 +159,20 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk } /** - * @dev Withdraws native token - * Callable only by transact method - * @param user Address of receiver - * @param tokenAmount Amount of token to withdraw - * @return spentAmount Actual spent amount + * @dev Converts given amount of tokens into native coins sent to the provided address. + * @param _user native coins receiver address. + * @param _tokenAmount amount to tokens to convert. + * @return actual converted amount, might be less than requested amount. */ - function _withdrawNative(address user, uint256 tokenAmount) internal virtual returns (uint256 spentAmount); + function _withdrawNative(address _user, uint256 _tokenAmount) internal virtual returns (uint256); /** - * @dev Finalizes permit deposit - * Callable only by transact method - * @param user Address of depositor - * @param nullifier Nullifier - * @param tokenAmount Amount of token to deposit + * @dev Performs token transfer using a signed permit signature. + * @param _user token depositor address, should correspond to the signature author. + * @param _nullifier nullifier and permit signature salt to avoid transaction data manipulation. + * @param _tokenAmount amount to tokens to deposit. */ - function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) internal virtual; + function _transferFromByPermit(address _user, uint256 _nullifier, int256 _tokenAmount) internal virtual; /** * @dev Perform a zkBob pool transaction. From deb43ed88aef89d8f65a02804df317142361528f Mon Sep 17 00:00:00 2001 From: Alexey Saplin <40490358+AlexSaplin@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:11:05 +0300 Subject: [PATCH 10/11] fix --- src/zkbob/ZkBobPoolERC20.sol | 12 ++++++------ src/zkbob/ZkBobPoolETH.sol | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/zkbob/ZkBobPoolERC20.sol b/src/zkbob/ZkBobPoolERC20.sol index a0193bd..65aabe9 100644 --- a/src/zkbob/ZkBobPoolERC20.sol +++ b/src/zkbob/ZkBobPoolERC20.sol @@ -56,20 +56,20 @@ contract ZkBobPoolERC20 is ZkBobPool { } // @inheritdoc ZkBobPool - function _withdrawNative(address user, uint256 tokenAmount) internal override returns (uint256 spentAmount) { + function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256 spentAmount) { ITokenSeller seller = tokenSeller; if (address(seller) != address(0)) { - IERC20(token).safeTransfer(address(seller), tokenAmount); - (, uint256 refunded) = seller.sellForETH(user, tokenAmount); - return tokenAmount - refunded; + IERC20(token).safeTransfer(address(seller), _tokenAmount); + (, uint256 refunded) = seller.sellForETH(_user, _tokenAmount); + return _tokenAmount - refunded; } } // @inheritdoc ZkBobPool - function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) internal override { + function _transferFromByPermit(address _user, uint256 _nullifier, int256 _tokenAmount) internal override { (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); IERC20Permit(token).receiveWithSaltedPermit( - user, uint256(tokenAmount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(nullifier), v, r, s + _user, uint256(_tokenAmount) * TOKEN_DENOMINATOR, _memo_permit_deadline(), bytes32(_nullifier), v, r, s ); } } diff --git a/src/zkbob/ZkBobPoolETH.sol b/src/zkbob/ZkBobPoolETH.sol index 3658618..2ade43c 100644 --- a/src/zkbob/ZkBobPoolETH.sol +++ b/src/zkbob/ZkBobPoolETH.sol @@ -49,17 +49,17 @@ contract ZkBobPoolETH is ZkBobPool { } // @inheritdoc ZkBobPool - function _withdrawNative(address user, uint256 tokenAmount) internal override returns (uint256 spentAmount) { - IWETH9(token).withdraw(tokenAmount); - if (!payable(user).send(tokenAmount)) { - IWETH9(token).deposit{value: tokenAmount}(); - IWETH9(token).transfer(user, tokenAmount); + function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256 spentAmount) { + IWETH9(token).withdraw(_tokenAmount); + if (!payable(_user).send(_tokenAmount)) { + IWETH9(token).deposit{value: _tokenAmount}(); + IWETH9(token).transfer(_user, _tokenAmount); } - return tokenAmount; + return _tokenAmount; } // @inheritdoc ZkBobPool - function _transferFromByPermit(address user, uint256 nullifier, int256 tokenAmount) internal override { + function _transferFromByPermit(address _user, uint256 _nullifier, int256 _tokenAmount) internal override { (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); bytes memory depositSignature = new bytes(65); @@ -72,15 +72,15 @@ contract ZkBobPoolETH is ZkBobPool { permit2.permitTransferFrom( IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: token, amount: uint256(tokenAmount) * TOKEN_DENOMINATOR}), - nonce: nullifier, + permitted: IPermit2.TokenPermissions({token: token, amount: uint256(_tokenAmount) * TOKEN_DENOMINATOR}), + nonce: _nullifier, deadline: uint256(_memo_permit_deadline()) }), IPermit2.SignatureTransferDetails({ to: address(this), - requestedAmount: uint256(tokenAmount) * TOKEN_DENOMINATOR + requestedAmount: uint256(_tokenAmount) * TOKEN_DENOMINATOR }), - user, + _user, depositSignature ); } From 6e4a400ef4a6dd689008821e451307059eda6b8d Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Fri, 7 Apr 2023 11:28:06 +0400 Subject: [PATCH 11/11] Remove unused return value names --- src/zkbob/ZkBobPoolERC20.sol | 3 ++- src/zkbob/ZkBobPoolETH.sol | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/zkbob/ZkBobPoolERC20.sol b/src/zkbob/ZkBobPoolERC20.sol index 65aabe9..51d3aa7 100644 --- a/src/zkbob/ZkBobPoolERC20.sol +++ b/src/zkbob/ZkBobPoolERC20.sol @@ -56,13 +56,14 @@ contract ZkBobPoolERC20 is ZkBobPool { } // @inheritdoc ZkBobPool - function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256 spentAmount) { + function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256) { ITokenSeller seller = tokenSeller; if (address(seller) != address(0)) { IERC20(token).safeTransfer(address(seller), _tokenAmount); (, uint256 refunded) = seller.sellForETH(_user, _tokenAmount); return _tokenAmount - refunded; } + return 0; } // @inheritdoc ZkBobPool diff --git a/src/zkbob/ZkBobPoolETH.sol b/src/zkbob/ZkBobPoolETH.sol index 2ade43c..69144c2 100644 --- a/src/zkbob/ZkBobPoolETH.sol +++ b/src/zkbob/ZkBobPoolETH.sol @@ -49,7 +49,7 @@ contract ZkBobPoolETH is ZkBobPool { } // @inheritdoc ZkBobPool - function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256 spentAmount) { + function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256) { IWETH9(token).withdraw(_tokenAmount); if (!payable(_user).send(_tokenAmount)) { IWETH9(token).deposit{value: _tokenAmount}();