From e7e2255e7310c17f870145a29aff2fb338e38160 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Tue, 6 Jun 2023 01:20:29 +0400 Subject: [PATCH] Customizable zkBob pools for different tokens (#66) --- script/scripts/Env.s.sol | 14 +- script/scripts/Local.s.sol | 9 +- ...20Impl.s.sol => NewZkBobPoolBOBImpl.s.sol} | 11 +- script/scripts/NewZkBobPoolETHImpl.s.sol | 3 - .../{ZkBobPoolERC20.s.sol => ZkBobPool.s.sol} | 57 +- script/scripts/ZkBobPoolETH.s.sol | 98 --- src/interfaces/IUSDCPermit.sol | 18 + src/utils/Sacrifice.sol | 9 - src/utils/UniswapV3Seller.sol | 13 +- src/zkbob/ZkBobDirectDepositQueue.sol | 6 +- src/zkbob/ZkBobDirectDepositQueueETH.sol | 8 +- src/zkbob/ZkBobPermit2Mixin.sol | 45 ++ src/zkbob/ZkBobPool.sol | 18 +- src/zkbob/ZkBobPoolBOB.sol | 33 + src/zkbob/ZkBobPoolERC20.sol | 79 +-- src/zkbob/ZkBobPoolETH.sol | 90 +-- src/zkbob/ZkBobPoolUSDC.sol | 33 + src/zkbob/ZkBobSaltedPermitMixin.sol | 18 + src/zkbob/ZkBobTokenSellerMixin.sol | 37 ++ src/zkbob/ZkBobUSDCPermitMixin.sol | 27 + src/zkbob/ZkBobWETHMixin.sol | 24 + src/zkbob/utils/ZkBobAccounting.sol | 9 +- test/BobVault.t.sol | 1 - test/mocks/ZkBobAccountingMock.sol | 2 +- test/zkbob/ZkBobPool.t.sol | 585 +++++++++++++++--- test/zkbob/ZkBobPoolERC20.t.sol | 241 -------- test/zkbob/ZkBobPoolETH.t.sol | 209 ------- 27 files changed, 864 insertions(+), 833 deletions(-) rename script/scripts/{NewZkBobPoolERC20Impl.s.sol => NewZkBobPoolBOBImpl.s.sol} (59%) rename script/scripts/{ZkBobPoolERC20.s.sol => ZkBobPool.s.sol} (65%) delete mode 100644 script/scripts/ZkBobPoolETH.s.sol create mode 100644 src/interfaces/IUSDCPermit.sol delete mode 100644 src/utils/Sacrifice.sol create mode 100644 src/zkbob/ZkBobPermit2Mixin.sol create mode 100644 src/zkbob/ZkBobPoolBOB.sol create mode 100644 src/zkbob/ZkBobPoolUSDC.sol create mode 100644 src/zkbob/ZkBobSaltedPermitMixin.sol create mode 100644 src/zkbob/ZkBobTokenSellerMixin.sol create mode 100644 src/zkbob/ZkBobUSDCPermitMixin.sol create mode 100644 src/zkbob/ZkBobWETHMixin.sol delete mode 100644 test/zkbob/ZkBobPoolERC20.t.sol delete mode 100644 test/zkbob/ZkBobPoolETH.t.sol diff --git a/script/scripts/Env.s.sol b/script/scripts/Env.s.sol index ee65da9..22abec4 100644 --- a/script/scripts/Env.s.sol +++ b/script/scripts/Env.s.sol @@ -4,6 +4,13 @@ pragma solidity 0.8.15; import "forge-std/Script.sol"; +enum PoolType { + BOB, + ETH, + USDC, + ERC20 +} + // common address constant deployer = 0x39F0bD56c1439a22Ee90b4972c16b7868D161981; address constant admin = 0xd4a3D9Ca00fa1fD8833D560F9217458E61c446d8; @@ -17,7 +24,9 @@ bytes32 constant bobSalt = bytes32(uint256(285834900769)); // zkbob uint256 constant zkBobPoolId = 0; // 0 is reserved for Polygon MVP pool, do not use for other deployments +PoolType constant zkBobPoolType = PoolType.BOB; string constant zkBobVerifiers = "prodV1"; +address constant zkBobToken = 0xB0B195aEFA3650A6908f15CdaC7D92F8a5791B0B; uint256 constant zkBobInitialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; address constant zkBobRelayer = 0xc2c4AD59B78F4A0aFD0CDB8133E640Db08Fa5b90; address constant zkBobRelayerFeeReceiver = 0x758768EC473279c4B1Aa61FA5450745340D4B17d; @@ -31,6 +40,7 @@ uint256 constant zkBobDailyUserDirectDepositCap = 0; uint256 constant zkBobDirectDepositCap = 0; uint256 constant zkBobDirectDepositFee = 0.1 gwei; uint256 constant zkBobDirectDepositTimeout = 1 days; +address constant permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; // new zkbob impl address constant zkBobPool = 0x72e6B59D4a90ab232e55D4BB7ed2dD17494D62fB; @@ -52,7 +62,3 @@ 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 c2adda4..6faca9a 100644 --- a/script/scripts/Local.s.sol +++ b/script/scripts/Local.s.sol @@ -7,8 +7,7 @@ import "./Env.s.sol"; 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/ZkBobPoolBOB.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; @@ -44,7 +43,7 @@ contract DeployLocal is Script { EIP1967Proxy poolProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); EIP1967Proxy queueProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); - ZkBobPoolERC20 poolImpl = new ZkBobPoolERC20( + ZkBobPoolBOB poolImpl = new ZkBobPoolBOB( zkBobPoolId, address(bob), transferVerifier, @@ -66,9 +65,9 @@ contract DeployLocal is Script { ); poolProxy.upgradeToAndCall(address(poolImpl), initData); } - ZkBobPoolERC20 pool = ZkBobPoolERC20(address(poolProxy)); + ZkBobPoolBOB pool = ZkBobPoolBOB(address(poolProxy)); - ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(bob)); + ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(bob), 1_000_000_000); queueProxy.upgradeTo(address(queueImpl)); ZkBobDirectDepositQueue queue = ZkBobDirectDepositQueue(address(queueProxy)); diff --git a/script/scripts/NewZkBobPoolERC20Impl.s.sol b/script/scripts/NewZkBobPoolBOBImpl.s.sol similarity index 59% rename from script/scripts/NewZkBobPoolERC20Impl.s.sol rename to script/scripts/NewZkBobPoolBOBImpl.s.sol index 6e8bd88..d74ebcc 100644 --- a/script/scripts/NewZkBobPoolERC20Impl.s.sol +++ b/script/scripts/NewZkBobPoolBOBImpl.s.sol @@ -4,18 +4,15 @@ 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/ZkBobPoolERC20.sol"; +import "../../src/zkbob/ZkBobPoolBOB.sol"; -contract DeployNewZkBobPoolERC20Impl is Script { +contract DeployNewZkBobPoolBOBImpl is Script { function run() external { vm.startBroadcast(); - ZkBobPoolERC20 pool = ZkBobPoolERC20(zkBobPool); + ZkBobPoolBOB pool = ZkBobPoolBOB(zkBobPool); - ZkBobPoolERC20 impl = new ZkBobPoolERC20( + ZkBobPoolBOB impl = new ZkBobPoolBOB( pool.pool_id(), pool.token(), pool.transfer_verifier(), diff --git a/script/scripts/NewZkBobPoolETHImpl.s.sol b/script/scripts/NewZkBobPoolETHImpl.s.sol index f4a5220..ea0f5db 100644 --- a/script/scripts/NewZkBobPoolETHImpl.s.sol +++ b/script/scripts/NewZkBobPoolETHImpl.s.sol @@ -4,9 +4,6 @@ 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 { diff --git a/script/scripts/ZkBobPoolERC20.s.sol b/script/scripts/ZkBobPool.s.sol similarity index 65% rename from script/scripts/ZkBobPoolERC20.s.sol rename to script/scripts/ZkBobPool.s.sol index 90ecb38..75c8aff 100644 --- a/script/scripts/ZkBobPoolERC20.s.sol +++ b/script/scripts/ZkBobPool.s.sol @@ -5,12 +5,15 @@ 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/ZkBobDirectDepositQueue.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import "../../src/zkbob/ZkBobPoolBOB.sol"; +import "../../src/zkbob/ZkBobPoolETH.sol"; +import "../../src/zkbob/ZkBobPoolUSDC.sol"; import "../../src/zkbob/ZkBobPoolERC20.sol"; -contract DeployZkBobPoolERC20 is Script { +contract DeployZkBobPool is Script { function run() external { vm.startBroadcast(); @@ -33,14 +36,39 @@ contract DeployZkBobPoolERC20 is Script { EIP1967Proxy poolProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); EIP1967Proxy queueProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); - ZkBobPoolERC20 poolImpl = new ZkBobPoolERC20( - zkBobPoolId, - bobVanityAddr, - transferVerifier, - treeVerifier, - batchDepositVerifier, - address(queueProxy) - ); + ZkBobPool poolImpl; + if (zkBobPoolType == PoolType.ETH) { + poolImpl = new ZkBobPoolETH( + zkBobPoolId, zkBobToken, + transferVerifier, treeVerifier, batchDepositVerifier, + address(queueProxy), permit2 + ); + } else if (zkBobPoolType == PoolType.BOB) { + poolImpl = new ZkBobPoolBOB( + zkBobPoolId, zkBobToken, + transferVerifier, treeVerifier, batchDepositVerifier, + address(queueProxy) + ); + } else if (zkBobPoolType == PoolType.USDC) { + poolImpl = new ZkBobPoolUSDC( + zkBobPoolId, zkBobToken, + transferVerifier, treeVerifier, batchDepositVerifier, + address(queueProxy) + ); + } else if (zkBobPoolType == PoolType.ERC20) { + uint8 decimals = IERC20Metadata(zkBobToken).decimals(); + uint256 denominator = decimals > 9 ? 10 ** (decimals - 9) : 1; + uint256 precision = decimals > 9 ? 1_000_000_000 : 10 ** decimals; + poolImpl = new ZkBobPoolERC20( + zkBobPoolId, zkBobToken, + transferVerifier, treeVerifier, batchDepositVerifier, + address(queueProxy), permit2, + denominator, precision + ); + } else { + revert("Unknown pool type"); + } + bytes memory initData = abi.encodeWithSelector( ZkBobPool.initialize.selector, zkBobInitialRoot, @@ -53,9 +81,14 @@ contract DeployZkBobPoolERC20 is Script { zkBobDirectDepositCap ); poolProxy.upgradeToAndCall(address(poolImpl), initData); - ZkBobPoolERC20 pool = ZkBobPoolERC20(address(poolProxy)); + ZkBobPool pool = ZkBobPool(address(poolProxy)); - ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), bobVanityAddr); + ZkBobDirectDepositQueue queueImpl; + if (zkBobPoolType == PoolType.ETH) { + queueImpl = new ZkBobDirectDepositQueueETH(address(pool), zkBobToken, pool.denominator()); + } else { + queueImpl = new ZkBobDirectDepositQueue(address(pool), zkBobToken, pool.denominator()); + } queueProxy.upgradeTo(address(queueImpl)); ZkBobDirectDepositQueue queue = ZkBobDirectDepositQueue(address(queueProxy)); diff --git a/script/scripts/ZkBobPoolETH.s.sol b/script/scripts/ZkBobPoolETH.s.sol deleted file mode 100644 index 6eec9bb..0000000 --- a/script/scripts/ZkBobPoolETH.s.sol +++ /dev/null @@ -1,98 +0,0 @@ -// 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(payable(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/IUSDCPermit.sol b/src/interfaces/IUSDCPermit.sol new file mode 100644 index 0000000..e1f8591 --- /dev/null +++ b/src/interfaces/IUSDCPermit.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +interface IUSDCPermit { + function transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) + external; +} diff --git a/src/utils/Sacrifice.sol b/src/utils/Sacrifice.sol deleted file mode 100644 index 55b14ba..0000000 --- a/src/utils/Sacrifice.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.15; - -contract Sacrifice { - constructor(address _receiver) payable { - selfdestruct(payable(_receiver)); - } -} diff --git a/src/utils/UniswapV3Seller.sol b/src/utils/UniswapV3Seller.sol index 11087f3..2843031 100644 --- a/src/utils/UniswapV3Seller.sol +++ b/src/utils/UniswapV3Seller.sol @@ -8,7 +8,6 @@ import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryImmutableState.sol" import "@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol"; import "@uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol"; import "../interfaces/ITokenSeller.sol"; -import "./Sacrifice.sol"; /** * @title UniswapV3Seller @@ -56,7 +55,9 @@ contract UniswapV3Seller is ITokenSeller { uint256 amountOut = swapRouter.exactInput( ISwapRouter.ExactInputParams({ - path: abi.encodePacked(token0, fee0, token1, fee1, WETH), + path: token1 == address(0) + ? abi.encodePacked(token0, fee0, WETH) + : abi.encodePacked(token0, fee0, token1, fee1, WETH), recipient: address(this), deadline: block.timestamp, amountIn: _amount, @@ -65,7 +66,8 @@ contract UniswapV3Seller is ITokenSeller { ); WETH.withdraw(amountOut); if (!payable(_receiver).send(amountOut)) { - new Sacrifice{value: amountOut}(_receiver); + WETH.deposit{value: amountOut}(); + WETH.transfer(_receiver, amountOut); } uint256 remainingBalance = IERC20(token0).balanceOf(address(this)); if (remainingBalance + _amount > balance) { @@ -82,6 +84,9 @@ contract UniswapV3Seller is ITokenSeller { * @return received eth amount. */ function quoteSellForETH(uint256 _amount) external returns (uint256) { - return quoter.quoteExactInput(abi.encodePacked(token0, fee0, token1, fee1, WETH), _amount); + bytes memory path = token1 == address(0) + ? abi.encodePacked(token0, fee0, WETH) + : abi.encodePacked(token0, fee0, token1, fee1, WETH); + return quoter.quoteExactInput(path, _amount); } } diff --git a/src/zkbob/ZkBobDirectDepositQueue.sol b/src/zkbob/ZkBobDirectDepositQueue.sol index 819330b..bee2425 100644 --- a/src/zkbob/ZkBobDirectDepositQueue.sol +++ b/src/zkbob/ZkBobDirectDepositQueue.sol @@ -21,10 +21,11 @@ contract ZkBobDirectDepositQueue is IZkBobDirectDeposits, IZkBobDirectDepositQue using SafeERC20 for IERC20; uint256 internal constant R = 21888242871839275222246405745257275088548364400416034343698204186575808495617; - uint256 internal constant TOKEN_DENOMINATOR = 1_000_000_000; uint256 internal constant MAX_NUMBER_OF_DIRECT_DEPOSITS = 16; bytes4 internal constant MESSAGE_PREFIX_DIRECT_DEPOSIT_V1 = 0x00000001; + uint256 internal immutable TOKEN_DENOMINATOR; + address public immutable token; uint256 public immutable pool_id; address public immutable pool; @@ -50,10 +51,11 @@ contract ZkBobDirectDepositQueue is IZkBobDirectDeposits, IZkBobDirectDepositQue event RefundDirectDeposit(uint256 indexed nonce, address receiver, uint256 amount); event CompleteDirectDepositBatch(uint256[] indices); - constructor(address _pool, address _token) { + constructor(address _pool, address _token, uint256 _denominator) { require(Address.isContract(_token), "ZkBobDirectDepositQueue: not a contract"); pool = _pool; token = _token; + TOKEN_DENOMINATOR = _denominator; pool_id = uint24(IZkBobPool(_pool).pool_id()); } diff --git a/src/zkbob/ZkBobDirectDepositQueueETH.sol b/src/zkbob/ZkBobDirectDepositQueueETH.sol index 9c8b7c3..e9bf34c 100644 --- a/src/zkbob/ZkBobDirectDepositQueueETH.sol +++ b/src/zkbob/ZkBobDirectDepositQueueETH.sol @@ -20,7 +20,13 @@ import "./ZkBobDirectDepositQueue.sol"; * Queue for zkBob ETH direct deposits. */ contract ZkBobDirectDepositQueueETH is IZkBobDirectDepositsETH, ZkBobDirectDepositQueue { - constructor(address _pool, address _token) ZkBobDirectDepositQueue(_pool, _token) {} + constructor( + address _pool, + address _token, + uint256 _denominator + ) + ZkBobDirectDepositQueue(_pool, _token, _denominator) + {} /// @inheritdoc IZkBobDirectDepositsETH function directNativeDeposit( diff --git a/src/zkbob/ZkBobPermit2Mixin.sol b/src/zkbob/ZkBobPermit2Mixin.sol new file mode 100644 index 0000000..6d557ce --- /dev/null +++ b/src/zkbob/ZkBobPermit2Mixin.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "../interfaces/IPermit2.sol"; +import "./ZkBobPool.sol"; + +/** + * @title ZkBobPermit2Mixin + */ +abstract contract ZkBobPermit2Mixin is ZkBobPool { + IPermit2 public immutable permit2; + + constructor(address _permit2) { + require(Address.isContract(_permit2), "ZkBobPool: not a contract"); + permit2 = IPermit2(_permit2); + } + + // @inheritdoc ZkBobPool + 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); + + 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 + ); + } +} diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index 7875786..1157742 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.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"; @@ -24,15 +25,16 @@ import "../proxy/EIP1967Admin.sol"; /** * @title ZkBobPool - * Shielded transactions pool for BOB tokens. + * Shielded transactions pool */ abstract contract ZkBobPool 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 internal immutable TOKEN_DENOMINATOR; + uint256 public immutable pool_id; ITransferVerifier public immutable transfer_verifier; ITreeVerifier public immutable tree_verifier; @@ -59,8 +61,12 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk ITransferVerifier _transfer_verifier, ITreeVerifier _tree_verifier, IBatchDepositVerifier _batch_deposit_verifier, - address _direct_deposit_queue - ) { + address _direct_deposit_queue, + uint256 _denominator, + uint256 _precision + ) + ZkBobAccounting(_precision) + { 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"); @@ -73,6 +79,8 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk tree_verifier = _tree_verifier; batch_deposit_verifier = _batch_deposit_verifier; direct_deposit_queue = IZkBobDirectDepositQueue(_direct_deposit_queue); + + TOKEN_DENOMINATOR = _denominator; } /** @@ -138,7 +146,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Zk * @dev Tells the denominator for converting BOB into zkBOB units. * 1e18 BOB units = 1e9 zkBOB units. */ - function denominator() external pure returns (uint256) { + function denominator() external view returns (uint256) { return TOKEN_DENOMINATOR; } diff --git a/src/zkbob/ZkBobPoolBOB.sol b/src/zkbob/ZkBobPoolBOB.sol new file mode 100644 index 0000000..33466b9 --- /dev/null +++ b/src/zkbob/ZkBobPoolBOB.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "./ZkBobPool.sol"; +import "./ZkBobTokenSellerMixin.sol"; +import "./ZkBobSaltedPermitMixin.sol"; + +/** + * @title ZkBobPoolBOB + * Shielded transactions pool for BOB tokens. + */ +contract ZkBobPoolBOB is ZkBobPool, ZkBobTokenSellerMixin, ZkBobSaltedPermitMixin { + 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, + 1_000_000_000, + 1_000_000_000 + ) + {} +} diff --git a/src/zkbob/ZkBobPoolERC20.sol b/src/zkbob/ZkBobPoolERC20.sol index 51d3aa7..e80b1ca 100644 --- a/src/zkbob/ZkBobPoolERC20.sol +++ b/src/zkbob/ZkBobPoolERC20.sol @@ -2,75 +2,36 @@ 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"; +import "./ZkBobTokenSellerMixin.sol"; +import "./ZkBobPermit2Mixin.sol"; /** - * @title ZkBobPool - * Shielded transactions pool for BOB tokens. + * @title ZkBobPoolERC20 + * Shielded transactions pool for ERC20 tokens */ -contract ZkBobPoolERC20 is ZkBobPool { - using SafeERC20 for IERC20; - - ITokenSeller public tokenSeller; - - event UpdateTokenSeller(address seller); - +contract ZkBobPoolERC20 is ZkBobPool, ZkBobTokenSellerMixin, ZkBobPermit2Mixin { constructor( uint256 __pool_id, address _token, ITransferVerifier _transfer_verifier, ITreeVerifier _tree_verifier, IBatchDepositVerifier _batch_deposit_verifier, - address _direct_deposit_queue + address _direct_deposit_queue, + address _permit2, + uint256 _denominator, + uint256 _precision ) - ZkBobPool(__pool_id, _token, _transfer_verifier, _tree_verifier, _batch_deposit_verifier, _direct_deposit_queue) + ZkBobPool( + __pool_id, + _token, + _transfer_verifier, + _tree_verifier, + _batch_deposit_verifier, + _direct_deposit_queue, + _denominator, + _precision + ) + ZkBobPermit2Mixin(_permit2) {} - - /** - * @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) { - 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 - 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 69144c2..ae4a9e2 100644 --- a/src/zkbob/ZkBobPoolETH.sol +++ b/src/zkbob/ZkBobPoolETH.sol @@ -2,37 +2,15 @@ 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"; +import "./ZkBobWETHMixin.sol"; +import "./ZkBobPermit2Mixin.sol"; /** - * @title ZkBobETHPool - * Shielded transactions pool for native and wrappred native tokens. + * @title ZkBobPoolETH + * Shielded transactions pool for native and wrapped native tokens. */ -contract ZkBobPoolETH is ZkBobPool { - using SafeERC20 for IERC20; - - IPermit2 public immutable permit2; - +contract ZkBobPoolETH is ZkBobPool, ZkBobWETHMixin, ZkBobPermit2Mixin { constructor( uint256 __pool_id, address _token, @@ -42,50 +20,16 @@ contract ZkBobPoolETH is ZkBobPool { 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) { - IWETH9(token).withdraw(_tokenAmount); - if (!payable(_user).send(_tokenAmount)) { - IWETH9(token).deposit{value: _tokenAmount}(); - IWETH9(token).transfer(_user, _tokenAmount); - } - return _tokenAmount; - } - - // @inheritdoc ZkBobPool - 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); - - 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 - ); - } - - receive() external payable { - require(msg.sender == address(token), "Not a WETH withdrawal"); - } + ZkBobPool( + __pool_id, + _token, + _transfer_verifier, + _tree_verifier, + _batch_deposit_verifier, + _direct_deposit_queue, + 1_000_000_000, + 1_000_000_000 + ) + ZkBobPermit2Mixin(_permit2) + {} } diff --git a/src/zkbob/ZkBobPoolUSDC.sol b/src/zkbob/ZkBobPoolUSDC.sol new file mode 100644 index 0000000..b846d73 --- /dev/null +++ b/src/zkbob/ZkBobPoolUSDC.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "./ZkBobPool.sol"; +import "./ZkBobTokenSellerMixin.sol"; +import "./ZkBobUSDCPermitMixin.sol"; + +/** + * @title ZkBobPoolUSDC + * Shielded transactions pool for USDC tokens supporting USDC transfer authorizations + */ +contract ZkBobPoolUSDC is ZkBobPool, ZkBobTokenSellerMixin, ZkBobUSDCPermitMixin { + 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, + 1, + 1_000_000 + ) + {} +} diff --git a/src/zkbob/ZkBobSaltedPermitMixin.sol b/src/zkbob/ZkBobSaltedPermitMixin.sol new file mode 100644 index 0000000..9e0b849 --- /dev/null +++ b/src/zkbob/ZkBobSaltedPermitMixin.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "./ZkBobPool.sol"; + +/** + * @title ZkBobSaltedPermitMixin + */ +abstract contract ZkBobSaltedPermitMixin is ZkBobPool { + // @inheritdoc ZkBobPool + 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/ZkBobTokenSellerMixin.sol b/src/zkbob/ZkBobTokenSellerMixin.sol new file mode 100644 index 0000000..2bca504 --- /dev/null +++ b/src/zkbob/ZkBobTokenSellerMixin.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "./ZkBobPool.sol"; + +/** + * @title ZkBobTokenSellerMixin + */ +abstract contract ZkBobTokenSellerMixin is ZkBobPool { + using SafeERC20 for IERC20; + + ITokenSeller public tokenSeller; + + event UpdateTokenSeller(address seller); + + /** + * @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) { + 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; + } +} diff --git a/src/zkbob/ZkBobUSDCPermitMixin.sol b/src/zkbob/ZkBobUSDCPermitMixin.sol new file mode 100644 index 0000000..2c8c091 --- /dev/null +++ b/src/zkbob/ZkBobUSDCPermitMixin.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "../interfaces/IUSDCPermit.sol"; +import "./ZkBobPool.sol"; + +/** + * @title ZkBobUSDCPermitMixin + */ +abstract contract ZkBobUSDCPermitMixin is ZkBobPool { + // @inheritdoc ZkBobPool + function _transferFromByPermit(address _user, uint256 _nullifier, int256 _tokenAmount) internal override { + (uint8 v, bytes32 r, bytes32 s) = _permittable_deposit_signature(); + IUSDCPermit(token).transferWithAuthorization( + _user, + address(this), + uint256(_tokenAmount) * TOKEN_DENOMINATOR, + 0, + _memo_permit_deadline(), + bytes32(_nullifier), + v, + r, + s + ); + } +} diff --git a/src/zkbob/ZkBobWETHMixin.sol b/src/zkbob/ZkBobWETHMixin.sol new file mode 100644 index 0000000..b8ad6ab --- /dev/null +++ b/src/zkbob/ZkBobWETHMixin.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import "./ZkBobPool.sol"; + +/** + * @title ZkBobWETHMixin + */ +abstract contract ZkBobWETHMixin is ZkBobPool { + // @inheritdoc ZkBobPool + function _withdrawNative(address _user, uint256 _tokenAmount) internal override returns (uint256) { + IWETH9(token).withdraw(_tokenAmount); + if (!payable(_user).send(_tokenAmount)) { + IWETH9(token).deposit{value: _tokenAmount}(); + IWETH9(token).transfer(_user, _tokenAmount); + } + return _tokenAmount; + } + + receive() external payable { + require(msg.sender == address(token), "Not a WETH withdrawal"); + } +} diff --git a/src/zkbob/utils/ZkBobAccounting.sol b/src/zkbob/utils/ZkBobAccounting.sol index fc9db45..bc7f296 100644 --- a/src/zkbob/utils/ZkBobAccounting.sol +++ b/src/zkbob/utils/ZkBobAccounting.sol @@ -13,11 +13,12 @@ import "./KycProvidersManagerStorage.sol"; * and overall transaction count does not exceed 4.3e9 (4.3 billion). Pool usage limits cannot exceed 4.3e9 BOB (4.3 billion) per day. */ contract ZkBobAccounting is KycProvidersManagerStorage { - uint256 internal constant PRECISION = 1_000_000_000; uint256 internal constant SLOT_DURATION = 1 hours; uint256 internal constant DAY_SLOTS = 1 days / SLOT_DURATION; uint256 internal constant WEEK_SLOTS = 1 weeks / SLOT_DURATION; + uint256 internal immutable PRECISION; + struct Slot0 { // max seen average tvl over period of at least 1 week (granularity of 1e9), might not be precise // max possible tvl - type(uint56).max * 1e9 zkBOB units ~= 7.2e16 BOB @@ -118,6 +119,10 @@ contract ZkBobAccounting is KycProvidersManagerStorage { event UpdateLimits(uint8 indexed tier, TierLimits limits); event UpdateTier(address user, uint8 tier); + constructor(uint256 _precision) { + PRECISION = _precision; + } + /** * @dev Returns currently configured limits and remaining quotas for the given user as of the current block. * @param _user user for which to retrieve limits. @@ -329,7 +334,7 @@ contract ZkBobAccounting is KycProvidersManagerStorage { emit UpdateLimits(_tier, tl); } - // Tier is set as per the KYC Providers Manager recomendation only in the case if no + // Tier is set as per the KYC Providers Manager recommendation only in the case if no // specific tier assigned to the user function _adjustConfiguredTierForUser(address _user, uint8 _configuredTier) internal view returns (uint8) { IKycProvidersManager kycProvidersMgr = kycProvidersManager(); diff --git a/test/BobVault.t.sol b/test/BobVault.t.sol index bb9b0b3..6bc53c8 100644 --- a/test/BobVault.t.sol +++ b/test/BobVault.t.sol @@ -242,7 +242,6 @@ abstract contract AbstractBobVault3poolTest is AbstractBobVaultTest, AbstractFor value = value - value * 0.002 ether / 1 ether; assertEq(dai.balanceOf(user1), 0); assertEq(usdc.balanceOf(user1), value); - vm.stopPrank(); } function testAmountEstimation() public { diff --git a/test/mocks/ZkBobAccountingMock.sol b/test/mocks/ZkBobAccountingMock.sol index 6bd97d2..0bd6e97 100644 --- a/test/mocks/ZkBobAccountingMock.sol +++ b/test/mocks/ZkBobAccountingMock.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.15; import "../../src/zkbob/utils/ZkBobAccounting.sol"; -contract ZkBobAccountingMock is ZkBobAccounting { +contract ZkBobAccountingMock is ZkBobAccounting(1_000_000_000) { uint256 tvl; uint56 public weekMaxTvl; uint32 public weekMaxCount; diff --git a/test/zkbob/ZkBobPool.t.sol b/test/zkbob/ZkBobPool.t.sol index 30ae805..f2149bf 100644 --- a/test/zkbob/ZkBobPool.t.sol +++ b/test/zkbob/ZkBobPool.t.sol @@ -3,33 +3,59 @@ pragma solidity 0.8.15; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.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 "../shared/ForkTests.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/ZkBobPool.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"; +import "../../src/interfaces/IERC677.sol"; +import "../../src/zkbob/ZkBobPoolUSDC.sol"; +import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; +import "../../src/zkbob/ZkBobPoolERC20.sol"; +import "../../src/zkbob/ZkBobPoolBOB.sol"; +import "../../src/zkbob/ZkBobPoolETH.sol"; +import "../../src/utils/UniswapV3Seller.sol"; -abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { +abstract contract AbstractZkBobPoolTest is AbstractForkTest { + address constant permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; - address constant uniV3Positions = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; - address constant usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address constant permit2Address = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + uint256 constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; + + enum PoolType { + BOB, + ETH, + USDC, + ERC20 + } + enum PermitType { + BOBPermit, + Permit2, + USDCPermit + } + + uint256 D; + address token; + address weth; + address tempToken; + bool autoApproveQueue; + PoolType poolType; + PermitType permitType; + uint256 denominator; + uint256 precision; bytes constant zkAddress = "QsnTijXekjRm9hKcq5kLNPsa6P4HtMRrc3RxVx3jsLHeo2AiysYxVJP86mriHfN"; @@ -37,6 +63,9 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { bytes32 constant PERMIT_TRANSFER_FROM_TYPEHASH = keccak256( "PermitTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" ); + bytes32 constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = keccak256( + "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" + ); event Message(uint256 indexed index, bytes32 indexed hash, bytes message); @@ -52,11 +81,76 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { IZkBobPoolAdmin pool; IZkBobDirectDepositsAdmin queue; - address token; IOperatorManager operatorManager; + function setUp() public { + vm.createSelectFork(forkRpcUrl, forkBlock); + + EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); + EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); + + ZkBobPool impl; + if (poolType == PoolType.ETH) { + impl = new ZkBobPoolETH( + 0, token, + new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), + address(queueProxy), permit2 + ); + } else if (poolType == PoolType.BOB) { + impl = new ZkBobPoolBOB( + 0, token, + new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), + address(queueProxy) + ); + } else if (poolType == PoolType.USDC) { + impl = new ZkBobPoolUSDC( + 0, token, + new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), + address(queueProxy) + ); + } else if (poolType == PoolType.ERC20) { + impl = new ZkBobPoolERC20( + 0, token, + new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), + address(queueProxy), permit2, 1_000_000_000, 1_000_000_000 + ); + } + + bytes memory initData = abi.encodeWithSelector( + ZkBobPool.initialize.selector, + initialRoot, + 1_000_000 ether / D, + 100_000 ether / D, + 100_000 ether / D, + 10_000 ether / D, + 10_000 ether / D, + 0, + 0 + ); + poolProxy.upgradeToAndCall(address(impl), initData); + pool = IZkBobPoolAdmin(address(poolProxy)); + + ZkBobDirectDepositQueue queueImpl; + if (poolType == PoolType.ETH) { + queueImpl = new ZkBobDirectDepositQueueETH(address(pool), token, denominator); + } else { + queueImpl = new ZkBobDirectDepositQueue(address(pool), token, denominator); + } + queueProxy.upgradeTo(address(queueImpl)); + queue = IZkBobDirectDepositsAdmin(address(queueProxy)); + + operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); + pool.setOperatorManager(operatorManager); + queue.setOperatorManager(operatorManager); + queue.setDirectDepositFee(uint64(0.1 ether / D)); + queue.setDirectDepositTimeout(1 days); + + deal(token, user1, 1 ether / D); + deal(token, user3, 0); + } + function testSimpleTransaction() public { - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); _transact(data1); bytes memory data2 = _encodeTransfer(); @@ -64,14 +158,14 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { vm.prank(user3); pool.withdrawFee(user2, user3); - assertEq(IERC20(token).balanceOf(user3), 0.02 ether); + assertEq(IERC20(token).balanceOf(user3), 0.02 ether / D); } function testGetters() public { assertEq(pool.pool_index(), 0); - assertEq(pool.denominator(), 1 gwei); + assertEq(pool.denominator(), denominator); - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); _transact(data1); assertEq(pool.pool_index(), 128); @@ -82,29 +176,52 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { 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))); + try pool.tokenSeller() { + vm.expectRevert("Ownable: caller is not the owner"); + pool.setTokenSeller(address(0)); + } catch {} + 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); + pool.setLimits( + 1, 2_000_000 ether / D, 200_000 ether / D, 200_000 ether / D, 20_000 ether / D, 20_000 ether / D, 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(user1).depositCap, 10_000 ether / D / denominator); assertEq(pool.getLimitsFor(user2).tier, 1); - assertEq(pool.getLimitsFor(user2).depositCap, 20_000 gwei); + assertEq(pool.getLimitsFor(user2).depositCap, 20_000 ether / D / denominator); } function testResetDailyLimits() public { - deal(token, user1, 10 ether); + deal(token, user1, 10 ether / D); - bytes memory data1 = _encodePermitDeposit(5 ether, 0.01 ether); + bytes memory data1 = _encodePermitDeposit(int256(5 ether / D), 0.01 ether / D); _transact(data1); - bytes memory data2 = _encodeWithdrawal(user1, 4 ether, 0 ether); + bytes memory data2 = _encodeWithdrawal(user1, 4 ether / D, 0); _transact(data2); - assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 5 gwei); - assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 gwei); + assertEq(pool.getLimitsFor(user1).dailyDepositCapUsage, 5 ether / D / denominator); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 ether / D / denominator); pool.resetDailyLimits(0); @@ -122,108 +239,142 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { } function testPermitDeposit() public { - bytes memory data = _encodePermitDeposit(0.5 ether, 0.01 ether); + bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); _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); + assertEq(IERC20(token).balanceOf(user1), 0.49 ether / D); + assertEq(IERC20(token).balanceOf(address(pool)), 0.5 ether / D); + assertEq(IERC20(token).balanceOf(user3), 0.01 ether / D); + } + + function testMultiplePermitDeposits() public { + for (uint256 i = 1; i < 10; i++) { + deal(address(token), user1, 0.101 ether / D * i); + bytes memory data = _encodePermitDeposit(int256(0.1 ether / D) * int256(i), 0.001 ether / D * i); + _transact(data); + } + + bytes memory data2 = _encodeTransfer(); + _transact(data2); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + assertEq(IERC20(token).balanceOf(user3), 0.055 ether / D); } function testUsualDeposit() public { vm.prank(user1); - IERC20(token).approve(address(pool), 0.51 ether); + IERC20(token).approve(address(pool), 0.51 ether / D); - bytes memory data = _encodeDeposit(0.5 ether, 0.01 ether); + bytes memory data = _encodeDeposit(int256(0.5 ether / D), 0.01 ether / D); _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); + assertEq(IERC20(token).balanceOf(user1), 0.49 ether / D); + assertEq(IERC20(token).balanceOf(address(pool)), 0.5 ether / D); + assertEq(IERC20(token).balanceOf(user3), 0.01 ether / D); } function testWithdrawal() public { - bytes memory data1 = _encodePermitDeposit(0.5 ether, 0.01 ether); + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); _transact(data1); - bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether, 0 ether); + bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether / D, 0); _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); + assertEq(IERC20(token).balanceOf(user1), 0.59 ether / D); + assertEq(IERC20(token).balanceOf(address(pool)), 0.39 ether / D); + assertEq(IERC20(token).balanceOf(user3), 0.02 ether / D); } function testRejectNegativeDeposits() public { - bytes memory data1 = _encodePermitDeposit(0.99 ether, 0.01 ether); + bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.01 ether / D); _transact(data1); - bytes memory data2 = _encodePermitDeposit(-0.5 ether, 1 ether); + bytes memory data2 = _encodePermitDeposit(-int256(0.5 ether / D), 1 ether / D); _transactReverted(data2, "ZkBobPool: incorrect deposit amounts"); vm.prank(user1); - IERC20(token).approve(address(pool), 0.5 ether); + IERC20(token).approve(address(pool), 0.5 ether / D); - bytes memory data3 = _encodeDeposit(-0.5 ether, 1 ether); + bytes memory data3 = _encodeDeposit(-int256(0.5 ether / D), 1 ether / D); _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); + deal(user1, 100 ether / D); + deal(user2, 100 ether / D); + deal(address(token), user1, 100 ether / D); + deal(address(token), user2, 100 ether / D); + + pool.setLimits( + 1, + 2_000_000 ether / D, + 200_000 ether / D, + 200_000 ether / D, + 20_000 ether / D, + 20_000 ether / D, + 25 ether / D, + 10 ether / D + ); address[] memory users = new address[](1); users[0] = user1; pool.setUsersTier(1, users); - queue.setDirectDepositFee(0.1 gwei); + queue.setDirectDepositFee(uint64(0.1 ether / D / pool.denominator())); + + if (autoApproveQueue) { + vm.prank(user1); + IERC20(token).approve(address(queue), type(uint256).max); + vm.prank(user2); + IERC20(token).approve(address(queue), type(uint256).max); + } } function testDirectDepositSubmit() public { _setUpDD(); - vm.prank(user2); + vm.startPrank(user2); vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); - _transferAndCall(10 ether, user2, zkAddress); + _directDeposit(10 ether / D, user2, zkAddress); + vm.stopPrank(); vm.startPrank(user1); vm.expectRevert("ZkBobDirectDepositQueue: direct deposit amount is too low"); - _transferAndCall(0.01 ether, user2, zkAddress); + _directDeposit(0.01 ether / D, user2, zkAddress); vm.expectRevert(ZkAddress.InvalidZkAddressLength.selector); - _transferAndCall(10 ether, user2, "invalid"); + _directDeposit(10 ether / D, user2, "invalid"); vm.expectRevert("ZkBobAccounting: single direct deposit cap exceeded"); - _transferAndCall(15 ether, user2, zkAddress); + _directDeposit(15 ether / D, user2, zkAddress); ZkAddress.ZkAddress memory parsedZkAddress = ZkAddress.parseZkAddress(zkAddress, 0); vm.expectEmit(true, true, false, true); - emit SubmitDirectDeposit(user1, 0, user2, parsedZkAddress, 9.9 gwei); - _transferAndCall(10 ether, user2, zkAddress); + emit SubmitDirectDeposit(user1, 0, user2, parsedZkAddress, uint64(9.9 ether / D / denominator)); + _directDeposit(10 ether / D, user2, zkAddress); - IERC20(token).approve(address(queue), 10 ether); + if (!autoApproveQueue) { + IERC20(token).approve(address(queue), 10 ether / D); + } vm.expectEmit(true, true, false, true); - emit SubmitDirectDeposit(user1, 1, user2, parsedZkAddress, 9.9 gwei); - queue.directDeposit(user2, 10 ether, zkAddress); + emit SubmitDirectDeposit(user1, 1, user2, parsedZkAddress, uint64(9.9 ether / D / denominator)); + queue.directDeposit(user2, 10 ether / D, zkAddress); vm.expectRevert("ZkBobAccounting: daily user direct deposit cap exceeded"); - _transferAndCall(10 ether, user2, zkAddress); + _directDeposit(10 ether / D, 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(deposit.sent, 10 ether / D); + assertEq(deposit.deposit, uint64(9.9 ether / D / denominator)); + assertEq(deposit.fee, 0.1 ether / D / denominator); assertEq(uint8(deposit.status), uint8(IZkBobDirectDeposits.DirectDepositStatus.Pending)); } vm.stopPrank(); @@ -232,11 +383,10 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { function testAppendDirectDeposits() public { _setUpDD(); - vm.prank(user1); - _transferAndCall(10 ether, user2, zkAddress); - - vm.prank(user1); - _transferAndCall(5 ether, user2, zkAddress); + vm.startPrank(user1); + _directDeposit(10 ether / D, user2, zkAddress); + _directDeposit(5 ether / D, user2, zkAddress); + vm.stopPrank(); uint256[] memory indices = new uint256[](2); indices[0] = 0; @@ -247,10 +397,10 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { outCommitment, bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(9.9 gwei), // first deposit amount + uint64(9.9 ether / D / denominator), // first deposit amount bytes10(0xc2767ac851b6b1e19eda), // second deposit receiver zk address (42 bytes) bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(4.9 gwei), // second deposit amount + uint64(4.9 ether / D / denominator), // second deposit amount new bytes(14 * 50) ); vm.expectCall( @@ -269,11 +419,11 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { uint64(0), // first deposit nonce bytes10(0xc2767ac851b6b1e19eda), // first deposit receiver zk address (42 bytes) bytes32(0x2f6f6ef223959602c05afd2b73ea8952fe0a10ad19ed665b3ee5a0b0b9e4e3ef), - uint64(9.9 gwei), // first deposit amount + uint64(9.9 ether / D / denominator), // 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 + uint64(4.9 ether / D / denominator) // second deposit amount ); vm.expectEmit(true, false, false, true); emit Message(128, bytes32(0), message); @@ -284,11 +434,10 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { function testRefundDirectDeposit() public { _setUpDD(); - vm.prank(user1); - _transferAndCall(10 ether + 1, user2, zkAddress); - - vm.prank(user1); - _transferAndCall(5 ether + 1, user2, zkAddress); + vm.startPrank(user1); + _directDeposit(10 ether / D + 1, user2, zkAddress); + _directDeposit(5 ether / D + 1, user2, zkAddress); + vm.stopPrank(); vm.expectRevert("ZkBobDirectDepositQueue: direct deposit timeout not passed"); queue.refundDirectDeposit(0); @@ -301,20 +450,20 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { vm.prank(user2); vm.expectEmit(true, false, false, true); - emit RefundDirectDeposit(0, user2, 10 ether + 1); + emit RefundDirectDeposit(0, user2, 10 ether / D + 1); queue.refundDirectDeposit(0); vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); queue.refundDirectDeposit(0); - assertEq(IERC20(token).balanceOf(user2), 10 ether + 1); + assertEq(IERC20(token).balanceOf(user2), 10 ether / D + 1); skip(2 days); vm.expectEmit(true, false, false, true); - emit RefundDirectDeposit(1, user2, 5 ether + 1); + emit RefundDirectDeposit(1, user2, 5 ether / D + 1); queue.refundDirectDeposit(1); vm.expectRevert("ZkBobDirectDepositQueue: direct deposit not pending"); queue.refundDirectDeposit(1); - assertEq(IERC20(token).balanceOf(user2), 15 ether + 2); + assertEq(IERC20(token).balanceOf(user2), 15 ether / D + 2); } function testDepositForUserWithKYCPassed() public { @@ -324,41 +473,107 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { SimpleKYCProviderManager manager = new SimpleKYCProviderManager(nft, tier); pool.setKycProvidersManager(manager); - pool.setLimits(tier, 50 ether, 10 ether, 2 ether, 6 ether, 5 ether, 0, 0); + pool.setLimits(tier, 50 ether / D, 10 ether / D, 2 ether / D, 6 ether / D, 5 ether / D, 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); + deal(address(token), address(user1), 10 ether / D); - bytes memory data = _encodePermitDeposit(4 ether, 0.01 ether); + bytes memory data = _encodePermitDeposit(int256(4 ether / D), 0.01 ether / D); _transact(data); - bytes memory data2 = _encodeWithdrawal(user1, 1 ether, 0 ether); + bytes memory data2 = _encodeWithdrawal(user1, 1 ether / D, 0); _transact(data2); - bytes memory data3 = _encodePermitDeposit(3 ether, 0.01 ether); + bytes memory data3 = _encodePermitDeposit(int256(3 ether / D), 0.01 ether / D); _transactReverted(data3, "ZkBobAccounting: daily user deposit cap exceeded"); - bytes memory data4 = _encodeWithdrawal(user1, 2 ether, 0 ether); + bytes memory data4 = _encodeWithdrawal(user1, 2 ether / D, 0); _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 + assertEq(pool.getLimitsFor(user1).dailyUserDepositCapUsage, 4 ether / D / denominator); + assertEq(pool.getLimitsFor(user1).dailyWithdrawalCapUsage, 1.01 ether / D / denominator); // 1 requested + 0.01 fees + } + + function _quoteNativeSwap(uint256 _amount) internal returns (uint256) { + if (poolType == PoolType.ETH) { + return _amount; + } + return pool.tokenSeller().quoteSellForETH(_amount); + } + + function testNativeWithdrawal() public { + if (poolType != PoolType.ETH) { + // enable token swaps for ETH + address addr; + if (tempToken == address(0)) { + addr = address(new UniswapV3Seller(uniV3Router, uniV3Quoter, token, 500, address(0), 0)); + } else { + addr = address(new UniswapV3Seller(uniV3Router, uniV3Quoter, token, 100, tempToken, 500)); + } + pool.setTokenSeller(addr); + assertEq(address(uint160(uint256(vm.load(address(pool), bytes32(uint256(11)))))), addr); + } + + vm.deal(user1, 0); + + bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.01 ether / D); + _transact(data1); + + // user1 withdraws 0.4 BOB, 0.3 BOB gets converted to ETH + uint256 quote2 = _quoteNativeSwap(0.3 ether / D); + bytes memory data2 = _encodeWithdrawal(user1, 0.4 ether / D, 0.3 ether / D); + _transact(data2); + + // user1 withdraws 0.2 BOB, trying to convert 0.3 BOB to ETH + bytes memory data4 = _encodeWithdrawal(user1, 0.2 ether / D, 0.3 ether / D); + vm.prank(user2); + (bool status, bytes memory returnData) = address(pool).call(data4); + assert(!status); + assertEq(returnData, stdError.arithmeticError); + + address dummy = address(new DummyImpl(0)); + uint256 quote3 = _quoteNativeSwap(0.3 ether / D); + bytes memory data3 = _encodeWithdrawal(dummy, 0.4 ether / D, 0.3 ether / D); + _transact(data3); + + vm.prank(user3); + pool.withdrawFee(user2, user3); + + assertEq(IERC20(token).balanceOf(user3), 0.03 ether / D); + assertEq(IERC20(token).balanceOf(address(pool)), 0.17 ether / D); + assertEq(IERC20(token).balanceOf(user1), 0.1 ether / D); + assertGt(user1.balance, 1 gwei); + assertEq(user1.balance, quote2); + + assertEq(dummy.balance, 0); + if (token == weth) { + assertEq(IERC20(token).balanceOf(dummy), 0.4 ether / D); + } else { + assertEq(IERC20(token).balanceOf(dummy), 0.1 ether / D); + assertGt(IERC20(weth).balanceOf(dummy), 1 gwei); + assertEq(IERC20(weth).balanceOf(dummy), quote3); + } } 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) + ZkBobPool.transact.selector, + nullifier, + _randFR(), + uint48(0), + uint112(0), + int64(_amount / int256(denominator)) ); 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()); + data = abi.encodePacked(data, uint16(0), uint16(44), uint64(_fee / denominator), bytes4(0x01000000), _randFR()); return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); } @@ -369,7 +584,7 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { _randFR(), uint48(0), uint112(0), - int64(-int256((_amount + 0.01 ether) / 1 gwei)) + int64(-int256((_amount / denominator) + 0.01 ether / D / denominator)) ); for (uint256 i = 0; i < 17; i++) { data = abi.encodePacked(data, _randFR()); @@ -378,8 +593,8 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { data, uint16(2), uint16(72), - uint64(0.01 ether / 1 gwei), - uint64(_nativeAmount / 1 gwei), + uint64(0.01 ether / D / denominator), + uint64(_nativeAmount / denominator), _to, bytes4(0x01000000), _randFR() @@ -388,12 +603,19 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { 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) + ZkBobPool.transact.selector, + _randFR(), + _randFR(), + uint48(0), + uint112(0), + -int64(uint64(0.01 ether / D / 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 / 1 gwei), bytes4(0x01000000), _randFR()); + return abi.encodePacked( + data, uint16(1), uint16(44), uint64(0.01 ether / D / denominator), bytes4(0x01000000), _randFR() + ); } function _transact(bytes memory _data) internal { @@ -418,7 +640,176 @@ abstract contract AbstractZkBobPoolTest is AbstractMainnetForkTest { return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; } - function _encodePermitDeposit(int256 _amount, uint256 _fee) internal virtual returns (bytes memory); + function _encodePermitDeposit(int256 _amount, uint256 _fee) internal returns (bytes memory) { + if (permitType == PermitType.Permit2) { + vm.prank(user1); + IERC20(token).approve(permit2, type(uint256).max); + } + + uint256 expiry = block.timestamp + 1 hours; + bytes32 nullifier = bytes32(_randFR()); + + bytes32 digest; + if (permitType == PermitType.BOBPermit) { + digest = _digestSaltedPermit(user1, address(pool), uint256(_amount + int256(_fee)), expiry, nullifier); + } else if (permitType == PermitType.Permit2) { + digest = _digestPermit2(user1, address(pool), uint256(_amount + int256(_fee)), expiry, nullifier); + } else if (permitType == PermitType.USDCPermit) { + digest = _digestUSDCPermit(user1, address(pool), uint256(_amount + int256(_fee)), expiry, nullifier); + } + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, digest); + + bytes memory data = abi.encodePacked( + ZkBobPool.transact.selector, + nullifier, + _randFR(), + uint48(0), + uint112(0), + int64(_amount / int256(denominator)) + ); + for (uint256 i = 0; i < 17; i++) { + data = abi.encodePacked(data, _randFR()); + } + data = abi.encodePacked( + data, + uint16(3), + uint16(72), + uint64(_fee / denominator), + uint64(expiry), + user1, + bytes4(0x01000000), + _randFR() + ); + return abi.encodePacked(data, r, uint256(s) + (v == 28 ? (1 << 255) : 0)); + } + + function _digestSaltedPermit( + address _holder, + address _spender, + uint256 _value, + uint256 _expiry, + bytes32 _salt + ) + internal + view + returns (bytes32) + { + uint256 nonce = IERC20Permit(token).nonces(_holder); + return ECDSA.toTypedDataHash( + IERC20Permit(token).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + IERC20Permit(token).SALTED_PERMIT_TYPEHASH(), _holder, _spender, _value, nonce, _expiry, _salt + ) + ) + ); + } + + function _digestPermit2( + address _holder, + address _spender, + uint256 _value, + uint256 _expiry, + bytes32 _salt + ) + internal + view + returns (bytes32) + { + return ECDSA.toTypedDataHash( + IPermit2(permit2).DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + PERMIT_TRANSFER_FROM_TYPEHASH, + keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, token, _value)), + _spender, + _salt, + _expiry + ) + ) + ); + } + + function _digestUSDCPermit( + address _holder, + address _spender, + uint256 _value, + uint256 _expiry, + bytes32 _salt + ) + internal + view + returns (bytes32) + { + return ECDSA.toTypedDataHash( + IERC20Permit(token).DOMAIN_SEPARATOR(), + keccak256(abi.encode(TRANSFER_WITH_AUTHORIZATION_TYPEHASH, _holder, _spender, _value, 0, _expiry, _salt)) + ); + } - function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal virtual; + function _directDeposit(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal { + if (poolType == PoolType.ETH) { + ZkBobDirectDepositQueueETH(address(queue)).directNativeDeposit{value: amount}(fallbackUser, _zkAddress); + } else if (poolType == PoolType.BOB) { + IERC677(token).transferAndCall(address(queue), amount, abi.encode(fallbackUser, _zkAddress)); + } else { + queue.directDeposit(fallbackUser, amount, _zkAddress); + } + } +} + +contract ZkBobPoolBOBPolygonTest is AbstractZkBobPoolTest, AbstractPolygonForkTest { + constructor() { + D = 1; + token = address(0xB0B195aEFA3650A6908f15CdaC7D92F8a5791B0B); + weth = address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + tempToken = address(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174); + poolType = PoolType.BOB; + autoApproveQueue = false; + permitType = PermitType.BOBPermit; + denominator = 1_000_000_000; + precision = 1_000_000_000; + } +} + +contract ZkBobPoolETHMainnetTest is AbstractZkBobPoolTest, AbstractMainnetForkTest { + constructor() { + D = 1; + token = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + tempToken = address(0); + poolType = PoolType.ETH; + autoApproveQueue = false; + permitType = PermitType.Permit2; + denominator = 1_000_000_000; + precision = 1_000_000_000; + } +} + +contract ZkBobPoolDAIMainnetTest is AbstractZkBobPoolTest, AbstractMainnetForkTest { + constructor() { + D = 1; + token = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); + weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + tempToken = address(0); + poolType = PoolType.ERC20; + autoApproveQueue = true; + permitType = PermitType.Permit2; + denominator = 1_000_000_000; + precision = 1_000_000_000; + } +} + +contract ZkBobPoolUSDCPolygonTest is AbstractZkBobPoolTest, AbstractPolygonForkTest { + constructor() { + D = 10 ** 12; + token = address(0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174); + weth = address(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + tempToken = address(0); + poolType = PoolType.USDC; + autoApproveQueue = true; + permitType = PermitType.USDCPermit; + denominator = 1; + precision = 1_000_000; + } } diff --git a/test/zkbob/ZkBobPoolERC20.t.sol b/test/zkbob/ZkBobPoolERC20.t.sol deleted file mode 100644 index cd1a28d..0000000 --- a/test/zkbob/ZkBobPoolERC20.t.sol +++ /dev/null @@ -1,241 +0,0 @@ -// 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/ZkBobPool.sol"; -import "../../src/zkbob/ZkBobPoolERC20.sol"; -import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; -import "../../src/BobToken.sol"; -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 AbstractZkBobPoolTest { - uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; - BobToken bob; - - function setUp() public { - EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, ""); - BobToken bobImpl = new BobToken(address(bobProxy)); - bobProxy.upgradeTo(address(bobImpl)); - bob = BobToken(address(bobProxy)); - bob.updateMinter(address(this), true, true); - - EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); - EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); - - ZkBobPoolERC20 impl = - new ZkBobPoolERC20(0, address(bob), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy)); - - bytes memory initData = abi.encodeWithSelector( - ZkBobPool.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 = IZkBobPoolAdmin(address(poolProxy)); - - ZkBobDirectDepositQueue queueImpl = new ZkBobDirectDepositQueue(address(pool), address(bob)); - queueProxy.upgradeTo(address(queueImpl)); - queue = IZkBobDirectDepositsAdmin(address(queueProxy)); - - operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); - pool.setOperatorManager(operatorManager); - queue.setOperatorManager(operatorManager); - queue.setDirectDepositFee(0.1 gwei); - queue.setDirectDepositTimeout(1 days); - - bob.mint(address(user1), 1 ether); - token = address(bob); - } - - 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 _setupNativeSwaps() internal { - vm.makePersistent(address(pool), address(bob)); - vm.makePersistent( - EIP1967Proxy(payable(address(pool))).implementation(), EIP1967Proxy(payable(address(bob))).implementation() - ); - vm.makePersistent( - address(pool.operatorManager()), address(pool.transfer_verifier()), address(pool.tree_verifier()) - ); - - // fork mainnet - vm.createSelectFork(forkRpcUrl, forkBlock); - - // create BOB-USDC 0.05% pool at Uniswap V3 - deal(usdc, address(this), 1e9); - IERC20(usdc).approve(uniV3Positions, 1e9); - address token0 = usdc; - address token1 = address(bob); - int24 tickLower = 276320; - int24 tickUpper = 276330; - uint256 amount0Desired = 1e9; - uint256 amount1Desired = 0; - uint160 price = TickMath.getSqrtRatioAtTick(tickLower); - if (token1 < token0) { - (token0, token1) = (token1, token0); - (tickLower, tickUpper) = (-tickUpper, -tickLower); - (amount0Desired, amount1Desired) = (amount1Desired, amount0Desired); - price = TickMath.getSqrtRatioAtTick(tickUpper); - } - INonfungiblePositionManager(uniV3Positions).createAndInitializePoolIfNecessary(token0, token1, 500, price); - INonfungiblePositionManager(uniV3Positions).mint( - INonfungiblePositionManager.MintParams({ - token0: token0, - token1: token1, - fee: 500, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: 0, - amount1Min: 0, - recipient: address(this), - deadline: block.timestamp - }) - ); - - // enable token swaps for ETH - pool.setTokenSeller(address(new UniswapV3Seller(uniV3Router, uniV3Quoter, address(bob), 500, usdc, 500))); - } - - function testNativeWithdrawal() public { - _setupNativeSwaps(); - - 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 = pool.tokenSeller().quoteSellForETH(0.3 ether); - bytes memory data2 = _encodeWithdrawal(user1, 0.4 ether, 0.3 ether); - _transact(data2); - - address dummy = address(new DummyImpl(0)); - uint256 quote3 = pool.tokenSeller().quoteSellForETH(0.3 ether); - bytes memory data3 = _encodeWithdrawal(dummy, 0.4 ether, 0.3 ether); - _transact(data3); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(bob.balanceOf(user1), 0.1 ether); - assertEq(bob.balanceOf(dummy), 0.1 ether); - assertEq(bob.balanceOf(address(pool)), 0.17 ether); - assertEq(bob.balanceOf(user3), 0.03 ether); - assertGt(user1.balance, 1 gwei); - assertEq(user1.balance, quote2); - assertGt(dummy.balance, 1 gwei); - assertEq(dummy.balance, quote3); - } - - function testNativeWithdrawalOutOfLiquidity() public { - _setupNativeSwaps(); - - bob.mint(address(user1), 9999.01 ether); - - vm.deal(user1, 0); - - bytes memory data1 = _encodePermitDeposit(10000 ether, 0.01 ether); - _transact(data1); - - uint256 quote2 = pool.tokenSeller().quoteSellForETH(60 ether); - bytes memory data2 = _encodeWithdrawal(user1, 100 ether, 60 ether); - _transact(data2); - - assertEq(bob.balanceOf(user1), 40 ether); - assertEq(bob.balanceOf(address(pool)), 9900.01 ether); - assertGt(user1.balance, 1 gwei); - - uint256 quote31 = pool.tokenSeller().quoteSellForETH(1000 ether); - uint256 quote32 = pool.tokenSeller().quoteSellForETH(3000 ether); - bytes memory data3 = _encodeWithdrawal(user1, 5000 ether, 3000 ether); - _transact(data3); - - vm.prank(user3); - pool.withdrawFee(user2, user3); - assertEq(bob.balanceOf(user3), 0.03 ether); - assertGt(bob.balanceOf(user1), 40 ether + 2000 ether + 2000 ether); - assertEq(bob.balanceOf(address(pool)), 4899.98 ether); - assertGt(user1.balance, 1 gwei); - assertEq(quote31, quote32); - assertEq(user1.balance, quote2 + quote31); - } - - function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal override { - bob.transferAndCall(address(queue), amount, abi.encode(fallbackUser, _zkAddress)); - } - - 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)), bob.nonces(user1), expiry, 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(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 _signSaltedPermit( - uint256 _pk, - address _holder, - address _spender, - uint256 _value, - uint256 _nonce, - uint256 _expiry, - bytes32 _salt - ) - internal - returns (uint8 v, bytes32 r, bytes32 s) - { - bytes32 digest = ECDSA.toTypedDataHash( - bob.DOMAIN_SEPARATOR(), - keccak256(abi.encode(bob.SALTED_PERMIT_TYPEHASH(), _holder, _spender, _value, _nonce, _expiry, _salt)) - ); - return vm.sign(_pk, digest); - } -} diff --git a/test/zkbob/ZkBobPoolETH.t.sol b/test/zkbob/ZkBobPoolETH.t.sol deleted file mode 100644 index 20863b1..0000000 --- a/test/zkbob/ZkBobPoolETH.t.sol +++ /dev/null @@ -1,209 +0,0 @@ -// 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 "@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 AbstractZkBobPoolTest { - uint256 private constant initialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; - - IPermit2 permit2; - - function setUp() public { - vm.createSelectFork(forkRpcUrl, forkBlock); - token = weth; - permit2 = IPermit2(permit2Address); - - EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); - EIP1967Proxy queueProxy = new EIP1967Proxy(address(this), address(0xdead), ""); - - ZkBobPoolETH impl = - new ZkBobPoolETH(0, address(token), new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), address(queueProxy), permit2Address); - - bytes memory initData = abi.encodeWithSelector( - ZkBobPool.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 = IZkBobPoolAdmin(payable(address(poolProxy))); - - ZkBobDirectDepositQueueETH queueImpl = new ZkBobDirectDepositQueueETH(address(pool), address(token)); - queueProxy.upgradeTo(address(queueImpl)); - queue = IZkBobDirectDepositsAdmin(address(queueProxy)); - - operatorManager = new MutableOperatorManager(user2, user3, "https://example.com"); - pool.setOperatorManager(operatorManager); - - queue.setOperatorManager(operatorManager); - queue.setDirectDepositFee(0.1 gwei); - queue.setDirectDepositTimeout(1 days); - - deal(weth, user1, 1 ether); - vm.startPrank(user1); - IERC20(token).approve(permit2Address, type(uint256).max); - IERC20(token).approve(address(queue), type(uint256).max); - vm.stopPrank(); - - vm.startPrank(user2); - IERC20(token).approve(permit2Address, type(uint256).max); - IERC20(token).approve(address(queue), type(uint256).max); - vm.stopPrank(); - - vm.startPrank(user3); - IERC20(token).approve(permit2Address, type(uint256).max); - IERC20(token).approve(address(queue), type(uint256).max); - 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); - - 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.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 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(IERC20(token).balanceOf(user1), 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); - assertEq(dummy.balance, 0); - } - - function _transferAndCall(uint256 amount, address fallbackUser, bytes memory _zkAddress) internal override { - queue.directNativeDeposit{value: amount}(fallbackUser, _zkAddress); - } - - 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)), 0, expiry, 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(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 _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 _nonce, - 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)); - } -}