diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index 1157742..30cdf52 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -190,7 +190,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk * Single transact() call performs either deposit, withdrawal or shielded transfer operation. */ function transact() external onlyOperator { - address user; + address user = msg.sender; uint256 txType = _tx_type(); if (txType == 0) { user = _deposit_spender(); @@ -200,6 +200,12 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk user = _memo_permit_holder(); } int256 transfer_token_delta = _transfer_token_amount(); + // For private transfers, operator can receive any fee amount. As receiving a fee is basically a withdrawal, + // we should consider operator's tier withdrawal limits respectfully. + // For deposits, fee transfers can be left unbounded, since they are paid from the deposits themselves, + // not from the pool funds. + // For withdrawals, withdrawal amount that is checked against limits for specific user is already inclusive + // of operator's fee, thus there is no need to consider it separately. (,, uint256 txCount) = _recordOperation(user, transfer_token_delta); uint256 nullifier = _transfer_nullifier(); @@ -291,7 +297,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk (uint256 total, uint256 totalFee, uint256 hashsum, bytes memory message) = direct_deposit_queue.collect(_indices, _out_commit); - uint256 txCount = _processDirectDepositBatch(total); + (,, uint256 txCount) = _recordOperation(address(0), int256(total)); uint256 _pool_index = txCount << 7; // verify that _out_commit corresponds to zero output account + 16 chosen notes + 111 empty notes diff --git a/src/zkbob/utils/ZkBobAccounting.sol b/src/zkbob/utils/ZkBobAccounting.sol index bc7f296..3b3dccb 100644 --- a/src/zkbob/utils/ZkBobAccounting.sol +++ b/src/zkbob/utils/ZkBobAccounting.sol @@ -203,6 +203,13 @@ contract ZkBobAccounting is KycProvidersManagerStorage { } function _processTVLChange(Slot1 memory s1, address _user, int256 _txAmount) internal { + // short path for direct deposits batch processing + if (_user == address(0) && _txAmount > 0) { + slot1.tvl += uint72(uint256(_txAmount)); + + return; + } + uint16 curDay = uint16(block.timestamp / SLOT_DURATION / DAY_SLOTS); UserStats memory us = userStats[_user]; @@ -288,11 +295,6 @@ contract ZkBobAccounting is KycProvidersManagerStorage { userStats[_user] = us; } - function _processDirectDepositBatch(uint256 _totalAmount) internal returns (uint256) { - slot1.tvl += uint72(_totalAmount); - return slot0.txCount++; - } - function _resetDailyLimits(uint8 _tier) internal { delete tiers[_tier].stats; } diff --git a/test/zkbob/ZkBobPool.t.sol b/test/zkbob/ZkBobPool.t.sol index f2149bf..ca4758d 100644 --- a/test/zkbob/ZkBobPool.t.sol +++ b/test/zkbob/ZkBobPool.t.sol @@ -153,7 +153,7 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); _transact(data1); - bytes memory data2 = _encodeTransfer(); + bytes memory data2 = _encodeTransfer(0.01 ether / D); _transact(data2); vm.prank(user3); @@ -170,7 +170,7 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { assertEq(pool.pool_index(), 128); - bytes memory data2 = _encodeTransfer(); + bytes memory data2 = _encodeTransfer(0.01 ether / D); _transact(data2); assertEq(pool.pool_index(), 256); @@ -256,7 +256,7 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { _transact(data); } - bytes memory data2 = _encodeTransfer(); + bytes memory data2 = _encodeTransfer(0.01 ether / D); _transact(data2); vm.prank(user3); @@ -559,6 +559,29 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { } } + function testOperatorCannotAvoidWithdrawalLimit() public { + deal(token, user1, 1_000_000 ether / D); + + pool.setLimits( + 0, 1_000_000 ether / D, 500_000 ether / D, 500_000 ether / D, 500_000 ether / D, 500_000 ether / D, 0, 0 + ); + bytes memory data1 = _encodePermitDeposit(int256(250_000 ether / D), 0.01 ether / D); + _transact(data1); + pool.setLimits( + 0, 1_000_000 ether / D, 100_000 ether / D, 100_000 ether / D, 10_000 ether / D, 10_000 ether / D, 0, 0 + ); + bytes memory data2 = _encodeTransfer(200_000 ether / D); + _transactReverted(data2, "ZkBobAccounting: daily withdrawal cap exceeded"); + + bytes memory data3 = _encodeTransfer(20_000 ether / D); + _transact(data3); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user3), 20_000.01 ether / D); + assertEq(pool.getLimitsFor(user2).dailyWithdrawalCapUsage, 20_000 ether / D / denominator); + } + 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)); @@ -601,21 +624,14 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { ); } - function _encodeTransfer() internal returns (bytes memory) { + function _encodeTransfer(uint256 _fee) internal returns (bytes memory) { bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, - _randFR(), - _randFR(), - uint48(0), - uint112(0), - -int64(uint64(0.01 ether / D / denominator)) + ZkBobPool.transact.selector, _randFR(), _randFR(), uint48(0), uint112(0), -int64(uint64(_fee / denominator)) ); for (uint256 i = 0; i < 17; i++) { data = abi.encodePacked(data, _randFR()); } - return abi.encodePacked( - data, uint16(1), uint16(44), uint64(0.01 ether / D / denominator), bytes4(0x01000000), _randFR() - ); + return abi.encodePacked(data, uint16(1), uint16(44), uint64(_fee / denominator), bytes4(0x01000000), _randFR()); } function _transact(bytes memory _data) internal {