diff --git a/LICENSE-MIT b/LICENSE-MIT index 82e8ac9..4ac1c7b 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,7 @@ MIT License Copyright (c) 2021 ZeroPool +Copyright (c) 2019 Matter Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/script/scripts/DeployAllowListOperatorManager.s.sol b/script/scripts/DeployAllowListOperatorManager.s.sol new file mode 100644 index 0000000..9817058 --- /dev/null +++ b/script/scripts/DeployAllowListOperatorManager.s.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Script.sol"; +import {AllowListOperatorManager} from "../../src/zkbob/manager/AllowListOperatorManager.sol"; + +// TODO: Update this values before the deployment +address constant operatorManagerOwner = 0x14fc6a1a996A2EB889cF86e5c8cD17323bC85290; +address constant zkBobProxy1 = 0x7D2D146a7AD3F0Dc398AA718a9bFCa2Bc873a5FD; +address constant zkBobProxyFeeReceiver1 = 0x7D2D146a7AD3F0Dc398AA718a9bFCa2Bc873a5FD; +address constant zkBobProxy2 = 0xFec49782FE8e11De9Fb3Ba645a76FE914FFfe3cb; +address constant zkBobProxyFeeReceiver2 = 0xFec49782FE8e11De9Fb3Ba645a76FE914FFfe3cb; +address constant zkBobProver1 = 0x33a0b018340d6424870cfC686a4d02e1df792254; +address constant zkBobProverFeeReceiver1 = 0x33a0b018340d6424870cfC686a4d02e1df792254; +address constant zkBobProver2 = 0x63A88E69fa7adEf036fc6ED94394CC9295de2f99; +address constant zkBobProverFeeReceiver2 = 0x63A88E69fa7adEf036fc6ED94394CC9295de2f99; + +bool constant allowListEnabled = true; + +contract DeployAllowListOperatorManager is Script { + function run() external { + vm.startBroadcast(); + + address[] memory operators = new address[](4); + operators[0] = zkBobProxy1; + operators[1] = zkBobProver1; + operators[2] = zkBobProxy2; + operators[3] = zkBobProver2; + + address[] memory feeReceivers = new address[](4); + feeReceivers[0] = zkBobProxyFeeReceiver1; + feeReceivers[1] = zkBobProverFeeReceiver1; + feeReceivers[2] = zkBobProxyFeeReceiver2; + feeReceivers[3] = zkBobProverFeeReceiver2; + + AllowListOperatorManager operatorManager = + new AllowListOperatorManager(operators, feeReceivers, allowListEnabled); + + operatorManager.transferOwnership(operatorManagerOwner); + + vm.stopBroadcast(); + + assert(address(operatorManager.owner()) == operatorManagerOwner); + + console2.log("AllowListOperatorManager address:", address(operatorManager)); + } +} diff --git a/script/scripts/DeployZkBobPoolModules.s.sol b/script/scripts/DeployZkBobPoolModules.s.sol deleted file mode 100644 index 1715179..0000000 --- a/script/scripts/DeployZkBobPoolModules.s.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.15; - -import "forge-std/Script.sol"; -import "forge-std/Test.sol"; -import "./Env.s.sol"; -import "../../src/zkbob/ZkBobPool.sol"; -import "../../src/zkbob/utils/ZkBobAccounting.sol"; -import "../../src/proxy/EIP1967Proxy.sol"; -import "../../src/zkbob/ZkBobPoolUSDC.sol"; - -contract DummyDelegateCall { - function delegate(address to, bytes calldata data) external { - (bool status,) = address(to).delegatecall(data); - require(status); - } -} - -contract Migrator { - function migrate(address _target, address _newImpl, address _accounting) external { - address kycManager = address(ZkBobAccounting(_target).kycProvidersManager()); - - EIP1967Proxy(payable(_target)).upgradeTo(_newImpl); - - bytes memory dump = ZkBobPool(_target).extsload(bytes32(uint256(1)), 2); - uint32 txCount = uint32(_load(dump, 0, 4)); - uint88 cumTvl = uint88(_load(dump, 4, 11)); - uint32 maxWeeklyTxCount = uint32(_load(dump, 21, 4)); - uint56 maxWeeklyAvgTvl = uint56(_load(dump, 25, 7)); - uint72 tvl = uint72(_load(dump, 55, 9)); - - ZkBobPool(_target).initializePoolIndex(txCount * 128); - ZkBobPool(_target).setAccounting(IZkBobAccounting(_accounting)); - ZkBobAccounting(_accounting).initialize(txCount + 1, tvl, cumTvl, maxWeeklyTxCount, maxWeeklyAvgTvl); - ZkBobAccounting(_accounting).setKycProvidersManager(IKycProvidersManager(kycManager)); - ZkBobAccounting(_accounting).setLimits( - 0, 2_000_000 gwei, 300_000 gwei, 300_000 gwei, 10_000 gwei, 10_000 gwei, 10_000 gwei, 1_000 gwei - ); - ZkBobAccounting(_accounting).setLimits( - 1, 2_000_000 gwei, 300_000 gwei, 300_000 gwei, 100_000 gwei, 100_000 gwei, 10_000 gwei, 1_000 gwei - ); - ZkBobAccounting(_accounting).setLimits( - 254, 2_000_000 gwei, 300_000 gwei, 300_000 gwei, 20_000 gwei, 20_000 gwei, 10_000 gwei, 1_000 gwei - ); - } - - function _load(bytes memory _dump, uint256 _from, uint256 _len) internal returns (uint256 res) { - assembly { - res := shr(sub(256, shl(3, _len)), mload(add(_dump, add(32, _from)))) - } - } -} - -contract DeployZkBobPoolModules is Script, Test { - function run() external { - ZkBobPoolUSDC pool = ZkBobPoolUSDC(address(zkBobPool)); - address owner = pool.owner(); - vm.etch(owner, type(DummyDelegateCall).runtimeCode); - - address tokenSeller = address(pool.tokenSeller()); - uint256 poolIndex = uint256(pool.pool_index()); - - vm.startBroadcast(); - - ZkBobPoolUSDC impl = new ZkBobPoolUSDC( - pool.pool_id(), pool.token(), pool.transfer_verifier(), pool.tree_verifier(), - pool.batch_deposit_verifier(), address(pool.direct_deposit_queue()) - ); - Migrator mig = new Migrator(); - ZkBobAccounting acc = new ZkBobAccounting(address(pool), 1_000_000_000); - acc.transferOwnership(owner); - DummyDelegateCall(owner).delegate( - address(mig), abi.encodeWithSelector(Migrator.migrate.selector, address(pool), address(impl), address(acc)) - ); - - vm.stopBroadcast(); - - acc.slot0(); - acc.slot1(); - - assertEq(address(pool.tokenSeller()), tokenSeller); - assertEq(uint256(pool.pool_index()), poolIndex); - } -} diff --git a/script/scripts/Env.s.sol b/script/scripts/Env.s.sol index 22abec4..8143cd6 100644 --- a/script/scripts/Env.s.sol +++ b/script/scripts/Env.s.sol @@ -12,9 +12,9 @@ enum PoolType { } // common -address constant deployer = 0x39F0bD56c1439a22Ee90b4972c16b7868D161981; -address constant admin = 0xd4a3D9Ca00fa1fD8833D560F9217458E61c446d8; -address constant owner = 0xd4a3D9Ca00fa1fD8833D560F9217458E61c446d8; +address constant deployer = 0x37493bFe9c8c31fAbe8615C988e83D59D1a667a9; +address constant admin = 0x37493bFe9c8c31fAbe8615C988e83D59D1a667a9; +address constant owner = 0x37493bFe9c8c31fAbe8615C988e83D59D1a667a9; address constant mockImpl = address(0xdead); // bob @@ -23,25 +23,51 @@ address constant bobVanityAddr = address(0xB0B195aEFA3650A6908f15CdaC7D92F8a5791 bytes32 constant bobSalt = bytes32(uint256(285834900769)); // zkbob -uint256 constant zkBobPoolId = 0; // 0 is reserved for Polygon MVP pool, do not use for other deployments +uint256 constant zkBobPoolId = 0xffff0a; // 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; +string constant zkBobVerifiers = "stageV2"; +address constant zkBobToken = 0x2C74B18e2f84B78ac67428d0c7a9898515f0c46f; uint256 constant zkBobInitialRoot = 11469701942666298368112882412133877458305516134926649826543144744382391691533; -address constant zkBobRelayer = 0xc2c4AD59B78F4A0aFD0CDB8133E640Db08Fa5b90; -address constant zkBobRelayerFeeReceiver = 0x758768EC473279c4B1Aa61FA5450745340D4B17d; -string constant zkBobRelayerURL = "https://relayer-mvp.zkbob.com"; -uint256 constant zkBobPoolCap = 1_000_000 ether; -uint256 constant zkBobDailyDepositCap = 100_000 ether; -uint256 constant zkBobDailyWithdrawalCap = 100_000 ether; -uint256 constant zkBobDailyUserDepositCap = 10_000 ether; -uint256 constant zkBobDepositCap = 10_000 ether; -uint256 constant zkBobDailyUserDirectDepositCap = 0; -uint256 constant zkBobDirectDepositCap = 0; +address constant zkBobProxy = 0xFec49782FE8e11De9Fb3Ba645a76FE914FFfe3cb; +address constant zkBobProxyFeeReceiver = 0xFec49782FE8e11De9Fb3Ba645a76FE914FFfe3cb; +address constant zkBobProver = 0x7D2D146a7AD3F0Dc398AA718a9bFCa2Bc873a5FD; +address constant zkBobProverFeeReceiver = 0x7D2D146a7AD3F0Dc398AA718a9bFCa2Bc873a5FD; uint256 constant zkBobDirectDepositFee = 0.1 gwei; uint256 constant zkBobDirectDepositTimeout = 1 days; address constant permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; +// decentralized +uint64 constant gracePeriod = 3 minutes; // TODO +uint64 constant minTreeUpdateFee = 0.1 gwei; +bool constant allowListEnabled = true; + +// accounting +address constant kycManager = address(0); + +uint256 constant tier0TvlCap = 2_000_000 gwei; +uint256 constant tier0DailyDepositCap = 300_000 gwei; +uint256 constant tier0DailyWithdrawalCap = 300_000 gwei; +uint256 constant tier0DailyUserDepositCap = 10_000 gwei; +uint256 constant tier0DepositCap = 10_000 gwei; +uint256 constant tier0DailyUserDirectDepositCap = 10_000 gwei; +uint256 constant tier0DirectDepositCap = 1_000 gwei; + +uint256 constant tier1TvlCap = 2_000_000 gwei; +uint256 constant tier1DailyDepositCap = 300_000 gwei; +uint256 constant tier1DailyWithdrawalCap = 300_000 gwei; +uint256 constant tier1DailyUserDepositCap = 100_000 gwei; +uint256 constant tier1DepositCap = 100_000 gwei; +uint256 constant tier1DailyUserDirectDepositCap = 10_000 gwei; +uint256 constant tier1DirectDepositCap = 1_000 gwei; + +uint256 constant tier254TvlCap = 2_000_000 gwei; +uint256 constant tier254DailyDepositCap = 300_000 gwei; +uint256 constant tier254DailyWithdrawalCap = 300_000 gwei; +uint256 constant tier254DailyUserDepositCap = 20_000 gwei; +uint256 constant tier254DepositCap = 20_000 gwei; +uint256 constant tier254DailyUserDirectDepositCap = 10_000 gwei; +uint256 constant tier254DirectDepositCap = 1_000 gwei; + // new zkbob impl address constant zkBobPool = 0x72e6B59D4a90ab232e55D4BB7ed2dD17494D62fB; diff --git a/script/scripts/Local.s.sol b/script/scripts/Local.s.sol index 7f33ba5..5536991 100644 --- a/script/scripts/Local.s.sol +++ b/script/scripts/Local.s.sol @@ -8,7 +8,7 @@ import "../../test/shared/EIP2470.t.sol"; import "../../src/BobToken.sol"; import "../../src/proxy/EIP1967Proxy.sol"; import "../../src/zkbob/ZkBobPoolBOB.sol"; -import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import {AllowListOperatorManager} from "../../src/zkbob/manager/AllowListOperatorManager.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; import "../../src/zkbob/utils/ZkBobAccounting.sol"; @@ -45,12 +45,7 @@ contract DeployLocal is Script { EIP1967Proxy queueProxy = new EIP1967Proxy(tx.origin, mockImpl, ""); ZkBobPoolBOB poolImpl = new ZkBobPoolBOB( - zkBobPoolId, - address(bob), - transferVerifier, - treeVerifier, - batchDepositVerifier, - address(queueProxy) + zkBobPoolId, address(bob), transferVerifier, treeVerifier, batchDepositVerifier, address(queueProxy) ); { bytes memory initData = abi.encodeWithSelector(ZkBobPool.initialize.selector, zkBobInitialRoot); @@ -64,28 +59,63 @@ contract DeployLocal is Script { { ZkBobAccounting accounting = new ZkBobAccounting(address(pool), 1_000_000_000); + if (kycManager != address(0)) { + accounting.setKycProvidersManager(IKycProvidersManager(kycManager)); + } accounting.setLimits( 0, - zkBobPoolCap, - zkBobDailyDepositCap, - zkBobDailyWithdrawalCap, - zkBobDailyUserDepositCap, - zkBobDepositCap, - zkBobDailyUserDirectDepositCap, - zkBobDirectDepositCap + tier0TvlCap, + tier0DailyDepositCap, + tier0DailyWithdrawalCap, + tier0DailyUserDepositCap, + tier0DepositCap, + tier0DailyUserDirectDepositCap, + tier0DirectDepositCap + ); + accounting.setLimits( + 1, + tier1TvlCap, + tier1DailyDepositCap, + tier1DailyWithdrawalCap, + tier1DailyUserDepositCap, + tier1DepositCap, + tier1DailyUserDirectDepositCap, + tier1DirectDepositCap + ); + accounting.setLimits( + 254, + tier254TvlCap, + tier254DailyDepositCap, + tier254DailyWithdrawalCap, + tier254DailyUserDepositCap, + tier254DepositCap, + tier254DailyUserDirectDepositCap, + tier254DirectDepositCap ); pool.setAccounting(accounting); } { - IOperatorManager operatorManager = - new MutableOperatorManager(zkBobRelayer, zkBobRelayerFeeReceiver, zkBobRelayerURL); + address[] memory operators = new address[](2); + operators[0] = zkBobProxy; + operators[1] = zkBobProver; + + address[] memory feeReceivers = new address[](2); + feeReceivers[0] = zkBobProxyFeeReceiver; + feeReceivers[1] = zkBobProverFeeReceiver; + + IOperatorManager operatorManager = new AllowListOperatorManager(operators, feeReceivers, allowListEnabled); pool.setOperatorManager(operatorManager); queue.setOperatorManager(operatorManager); queue.setDirectDepositFee(uint64(zkBobDirectDepositFee)); queue.setDirectDepositTimeout(uint40(zkBobDirectDepositTimeout)); } + { + pool.setGracePeriod(gracePeriod); + pool.setMinTreeUpdateFee(minTreeUpdateFee); + } + { if (owner != address(0)) { bob.transferOwnership(owner); diff --git a/script/scripts/MigrateDecentralization.s.sol b/script/scripts/MigrateDecentralization.s.sol new file mode 100644 index 0000000..e511582 --- /dev/null +++ b/script/scripts/MigrateDecentralization.s.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Script.sol"; +import "forge-std/Test.sol"; +import "../../src/zkbob/ZkBobPoolUSDC.sol"; +import "../../src/proxy/EIP1967Proxy.sol"; +import "../../src/zkbob/utils/ZkBobAccounting.sol"; +import "../../src/zkbob/manager/AllowListOperatorManager.sol"; + +// TODO: update this parameters before running the script +address constant newZkBobPoolImpl = 0x0114Bf30d9f5A7f503D3DFC65534F2B5AC302c85; +address constant newOperatorManager = 0xFd5a6a67D768d5BF1A8c7724387CA8786Bd4DD91; + +/** + * @dev OP-USDC pool proxy address. + */ +address constant zkBobPool = 0x1CA8C2B9B20E18e86d5b9a72370fC6c91814c97C; + +/** + * @dev This value should be sufficient for dedicated prover to update the tree + * but not too big to support liveness. + */ +uint64 constant gracePeriod = 3 minutes; + +/** + * @dev This value should cover the cost of the tree update. + */ +uint64 constant minTreeUpdateFee = 0.1 gwei; + +// Only for checks: +address constant relayer = 0xb9CD01c0b417b4e9095f620aE2f849A84a9B1690; + +contract UpgradeTest is Test { + struct PoolSnapshot { + address owner; + uint256 poolIndex; + uint256 oneNullifier; + uint256 lastRoot; + bytes32 all_messages_hash; + uint256 relayerFee; + address tokenSeller; + address accounting; + } + + function makeSnapshot(ZkBobPoolUSDC _pool) internal view returns (PoolSnapshot memory) { + return PoolSnapshot({ + owner: _pool.owner(), + poolIndex: _pool.pool_index(), + oneNullifier: _pool.nullifiers(0x39a833a5c374a0a3328f65ae9a9bf883945694cca613a8415c3a555bda388cd), + lastRoot: _pool.roots(_pool.pool_index()), + all_messages_hash: _pool.all_messages_hash(), + relayerFee: _pool.accumulatedFee(relayer), + tokenSeller: address(_pool.tokenSeller()), + accounting: address(_pool.accounting()) + }); + } + + function postCheck(ZkBobPoolUSDC _pool, PoolSnapshot memory _snapshot) internal { + assertEq(_snapshot.owner, _pool.owner()); + assertEq(_snapshot.poolIndex, uint256(_pool.pool_index())); + assertEq( + _snapshot.oneNullifier, _pool.nullifiers(0x39a833a5c374a0a3328f65ae9a9bf883945694cca613a8415c3a555bda388cd) + ); + assertEq(_snapshot.lastRoot, _pool.roots(_pool.pool_index())); + assertEq(_snapshot.all_messages_hash, _pool.all_messages_hash()); + assertEq(_snapshot.relayerFee, _pool.accumulatedFee(relayer)); + assertEq(_snapshot.tokenSeller, address(_pool.tokenSeller())); + assertEq(_snapshot.accounting, address(_pool.accounting())); + assertEq(gracePeriod, _pool.gracePeriod()); + assertEq(minTreeUpdateFee, _pool.minTreeUpdateFee()); + + vm.expectRevert("ZkBobPool: queue is empty"); + _pool.pendingCommitment(); + } +} + +/** + * @dev Don't forget to set ZkBobPool.TOKEN_NUMERATOR to 1000 for USDC pools. + */ +contract MigrateDecentralization is Script, UpgradeTest { + function run() external { + ZkBobPoolUSDC pool = ZkBobPoolUSDC(address(zkBobPool)); + PoolSnapshot memory snapshot = makeSnapshot(pool); + + vm.startBroadcast(); + + // 1. Upgrade proxy to new implementation + EIP1967Proxy(payable(address(pool))).upgradeTo(address(newZkBobPoolImpl)); + + // 2. Set grace period + ZkBobPool(pool).setGracePeriod(gracePeriod); + // 3. Set min tree update fee + ZkBobPool(pool).setMinTreeUpdateFee(minTreeUpdateFee); + // 4. Set token seller + ZkBobPoolUSDC(pool).setTokenSeller(snapshot.tokenSeller); + // 5. Set operator manager + ZkBobPool(pool).setOperatorManager(AllowListOperatorManager(newOperatorManager)); + + vm.stopBroadcast(); + + postCheck(ZkBobPoolUSDC(address(pool)), snapshot); + } +} diff --git a/script/scripts/NewZkBobPoolUSDCImpl.s.sol b/script/scripts/NewZkBobPoolUSDCImpl.s.sol new file mode 100644 index 0000000..50ae05e --- /dev/null +++ b/script/scripts/NewZkBobPoolUSDCImpl.s.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import "forge-std/Script.sol"; +import "../../src/zkbob/ZkBobPoolUSDC.sol"; + +/** + * @dev OP-USDC pool proxy address. + */ +address constant zkBobPool = 0x1CA8C2B9B20E18e86d5b9a72370fC6c91814c97C; + +contract DeployNewZkBobPoolUSDCImpl is Script { + function run() external { + ZkBobPoolUSDC pool = ZkBobPoolUSDC(payable(zkBobPool)); + + vm.startBroadcast(); + + ZkBobPoolUSDC newImpl = new ZkBobPoolUSDC( + pool.pool_id(), + pool.token(), + pool.transfer_verifier(), + pool.tree_verifier(), + pool.batch_deposit_verifier(), + address(pool.direct_deposit_queue()) + ); + + vm.stopBroadcast(); + + console2.log("ZkBobPoolUSDC implementation:", address(newImpl)); + } +} diff --git a/script/scripts/ZkBobPool.s.sol b/script/scripts/ZkBobPool.s.sol index 337c156..d5fe7b5 100644 --- a/script/scripts/ZkBobPool.s.sol +++ b/script/scripts/ZkBobPool.s.sol @@ -7,7 +7,7 @@ import "./Env.s.sol"; import "../../src/proxy/EIP1967Proxy.sol"; import "../../src/zkbob/ZkBobDirectDepositQueue.sol"; import "../../src/zkbob/ZkBobDirectDepositQueueETH.sol"; -import "../../src/zkbob/manager/MutableOperatorManager.sol"; +import {AllowListOperatorManager} from "../../src/zkbob/manager/AllowListOperatorManager.sol"; import "../../src/zkbob/ZkBobPoolBOB.sol"; import "../../src/zkbob/ZkBobPoolETH.sol"; import "../../src/zkbob/ZkBobPoolUSDC.sol"; @@ -53,27 +53,31 @@ contract DeployZkBobPool is Script { if (zkBobPoolType == PoolType.ETH) { vars.poolImpl = new ZkBobPoolETH( - zkBobPoolId, zkBobToken, - transferVerifier, treeVerifier, batchDepositVerifier, - address(vars.queueProxy), permit2 + zkBobPoolId, + zkBobToken, + transferVerifier, + treeVerifier, + batchDepositVerifier, + address(vars.queueProxy), + permit2 ); } else if (zkBobPoolType == PoolType.BOB) { vars.poolImpl = new ZkBobPoolBOB( - zkBobPoolId, zkBobToken, - transferVerifier, treeVerifier, batchDepositVerifier, - address(vars.queueProxy) + zkBobPoolId, zkBobToken, transferVerifier, treeVerifier, batchDepositVerifier, address(vars.queueProxy) ); } else if (zkBobPoolType == PoolType.USDC) { vars.poolImpl = new ZkBobPoolUSDC( - zkBobPoolId, zkBobToken, - transferVerifier, treeVerifier, batchDepositVerifier, - address(vars.queueProxy) + zkBobPoolId, zkBobToken, transferVerifier, treeVerifier, batchDepositVerifier, address(vars.queueProxy) ); } else if (zkBobPoolType == PoolType.ERC20) { vars.poolImpl = new ZkBobPoolERC20( - zkBobPoolId, zkBobToken, - transferVerifier, treeVerifier, batchDepositVerifier, - address(vars.queueProxy), permit2, + zkBobPoolId, + zkBobToken, + transferVerifier, + treeVerifier, + batchDepositVerifier, + address(vars.queueProxy), + permit2, vars.denominator ); } else { @@ -93,29 +97,67 @@ contract DeployZkBobPool is Script { vars.queueProxy.upgradeTo(address(queueImpl)); ZkBobDirectDepositQueue queue = ZkBobDirectDepositQueue(address(vars.queueProxy)); - IOperatorManager operatorManager = - new MutableOperatorManager(zkBobRelayer, zkBobRelayerFeeReceiver, zkBobRelayerURL); - pool.setOperatorManager(operatorManager); - queue.setOperatorManager(operatorManager); + AllowListOperatorManager operatorManager; + { + address[] memory operators = new address[](2); + operators[0] = zkBobProxy; + operators[1] = zkBobProver; + + address[] memory feeReceivers = new address[](2); + feeReceivers[0] = zkBobProxyFeeReceiver; + feeReceivers[1] = zkBobProverFeeReceiver; + + operatorManager = new AllowListOperatorManager(operators, feeReceivers, allowListEnabled); + pool.setOperatorManager(operatorManager); + queue.setOperatorManager(operatorManager); + } + queue.setDirectDepositFee(uint64(zkBobDirectDepositFee)); queue.setDirectDepositTimeout(uint40(zkBobDirectDepositTimeout)); ZkBobAccounting accounting = new ZkBobAccounting(address(pool), vars.precision); + if (kycManager != address(0)) { + accounting.setKycProvidersManager(IKycProvidersManager(kycManager)); + } accounting.setLimits( 0, - zkBobPoolCap, - zkBobDailyDepositCap, - zkBobDailyWithdrawalCap, - zkBobDailyUserDepositCap, - zkBobDepositCap, - zkBobDailyUserDirectDepositCap, - zkBobDirectDepositCap + tier0TvlCap, + tier0DailyDepositCap, + tier0DailyWithdrawalCap, + tier0DailyUserDepositCap, + tier0DepositCap, + tier0DailyUserDirectDepositCap, + tier0DirectDepositCap + ); + accounting.setLimits( + 1, + tier1TvlCap, + tier1DailyDepositCap, + tier1DailyWithdrawalCap, + tier1DailyUserDepositCap, + tier1DepositCap, + tier1DailyUserDirectDepositCap, + tier1DirectDepositCap + ); + accounting.setLimits( + 254, + tier254TvlCap, + tier254DailyDepositCap, + tier254DailyWithdrawalCap, + tier254DailyUserDepositCap, + tier254DepositCap, + tier254DailyUserDirectDepositCap, + tier254DirectDepositCap ); pool.setAccounting(accounting); + pool.setGracePeriod(gracePeriod); + pool.setMinTreeUpdateFee(minTreeUpdateFee); + if (owner != address(0)) { pool.transferOwnership(owner); queue.transferOwnership(owner); + operatorManager.transferOwnership(owner); } if (admin != tx.origin) { @@ -134,12 +176,18 @@ contract DeployZkBobPool is Script { 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"); + require(pool.gracePeriod() == gracePeriod, "Grace period is not configured"); + require(pool.minTreeUpdateFee() == minTreeUpdateFee, "Min tree update fee is not configured"); + require(address(pool.accounting()) == address(accounting), "Accounting is not configured"); + require(address(pool.operatorManager()) == address(operatorManager), "Operator manager is not configured"); + require(address(queue.operatorManager()) == address(operatorManager), "Operator manager is not configured"); console2.log("ZkBobPool:", address(pool)); console2.log("ZkBobPool implementation:", address(vars.poolImpl)); console2.log("ZkBobDirectDepositQueue:", address(queue)); console2.log("ZkBobDirectDepositQueue implementation:", address(queueImpl)); console2.log("ZkBobAccounting:", address(accounting)); + console2.log("AllowListOperatorManager:", address(operatorManager)); console2.log("TransferVerifier:", address(transferVerifier)); console2.log("TreeUpdateVerifier:", address(treeVerifier)); console2.log("BatchDepositVierifier:", address(batchDepositVerifier)); diff --git a/src/interfaces/IZkBobAccounting.sol b/src/interfaces/IZkBobAccounting.sol index f66bed3..24cb3f9 100644 --- a/src/interfaces/IZkBobAccounting.sol +++ b/src/interfaces/IZkBobAccounting.sol @@ -8,8 +8,7 @@ interface IZkBobAccounting { enum TxType { Common, DirectDeposit, - AppendDirectDeposits, - ForcedExit + AppendDirectDeposits } struct Limits { diff --git a/src/interfaces/IZkBobDirectDeposits.sol b/src/interfaces/IZkBobDirectDeposits.sol index 51caf36..72eedab 100644 --- a/src/interfaces/IZkBobDirectDeposits.sol +++ b/src/interfaces/IZkBobDirectDeposits.sol @@ -8,6 +8,7 @@ interface IZkBobDirectDeposits { Pending, // requested deposit was submitted and is pending in the queue Completed, // requested deposit was successfully processed Refunded // requested deposit was refunded to the fallback receiver + } struct DirectDeposit { diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index fb89938..4435aff 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -24,6 +24,7 @@ import "../utils/Ownable.sol"; import "../proxy/EIP1967Admin.sol"; import "../interfaces/IEnergyRedeemer.sol"; import "../utils/ExternalSload.sol"; +import {Queue, PendingCommitment} from "./utils/Queue.sol"; /** * @title ZkBobPool @@ -31,11 +32,11 @@ import "../utils/ExternalSload.sol"; */ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, ExternalSload { using SafeERC20 for IERC20; + using Queue for Queue.CommitmentQueue; uint256 internal constant MAX_POOL_ID = 0xffffff; bytes4 internal constant MESSAGE_PREFIX_COMMON_V1 = 0x00000000; - uint256 internal constant FORCED_EXIT_MIN_DELAY = 1 hours; - uint256 internal constant FORCED_EXIT_MAX_DELAY = 24 hours; + bytes4 internal constant MESSAGE_PREFIX_COMMON_V2 = 0x00000002; uint256 internal immutable TOKEN_DENOMINATOR; uint256 internal constant TOKEN_NUMERATOR = 1; @@ -47,8 +48,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex address public immutable token; IZkBobDirectDepositQueue public immutable direct_deposit_queue; - uint256[2] private __deprecatedGap; - mapping(uint256 => bytes32) public committedForcedExits; + uint256[3] private __deprecatedGap; IEnergyRedeemer public redeemer; IZkBobAccounting public accounting; uint96 public pool_index; @@ -61,18 +61,40 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex mapping(address => uint256) public accumulatedFee; + /** + * @dev It is the slot where tokenSeller was stored. + */ + address private __deprecatedGap2; + + /** + * @dev Queue of pending commitments to be included in the Merkle Tree. + */ + Queue.CommitmentQueue internal pendingCommitments; + + /** + * @dev The duration of the grace period during which only the privileged prover + * can submit the tree update proof. + */ + uint64 public gracePeriod; + + /** + * @dev The minimal fee required to be reserved for the prover who will submit the tree update proof. + * This fee is used to prevent spamming the pool with transactions that don't incentivize provers + * to provide tree update proof. + */ + uint64 public minTreeUpdateFee; + + uint256[50] __gap; + event UpdateOperatorManager(address manager); event UpdateAccounting(address accounting); event UpdateRedeemer(address redeemer); + event UpdateGracePeriod(uint64 gracePeriod); + event UpdateMinTreeUpdateFee(uint64 minTreeUpdateFee); event WithdrawFee(address indexed operator, uint256 fee); event Message(uint256 indexed index, bytes32 indexed hash, bytes message); - - event CommitForcedExit( - uint256 indexed nullifier, address operator, address to, uint256 amount, uint256 exitStart, uint256 exitEnd - ); - event CancelForcedExit(uint256 indexed nullifier); - event ForcedExit(uint256 indexed index, uint256 indexed nullifier, address to, uint256 amount); + event RootUpdated(uint256 indexed index, uint256 root, uint256 commitment); constructor( uint256 __pool_id, @@ -147,6 +169,24 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex return TOKEN_NUMERATOR == 1 ? TOKEN_DENOMINATOR : (1 << 255) | TOKEN_NUMERATOR; } + /** + * @dev Returns the first pending commitment in the prioirty queue. + * @return commitment commitment value. + * @return privilegedProver prover that can submit the tree update proof within the grace period. + * @return fee fee reserved for the prover who will submit the tree update proof. + * @return timestamp commitment timestamp. + * @return gracePeriodEnd timestamp when the grace period ends. + */ + function pendingCommitment() + external + view + returns (uint256 commitment, address privilegedProver, uint64 fee, uint64 timestamp, uint64 gracePeriodEnd) + { + PendingCommitment memory op = pendingCommitments.front(); + gracePeriodEnd = op.timestamp + gracePeriod; + return (op.commitment, op.prover, op.fee, op.timestamp, gracePeriodEnd); + } + /** * @dev Updates used accounting module. * Callable only by the contract owner / proxy admin. @@ -171,6 +211,27 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex emit UpdateRedeemer(address(_redeemer)); } + /** + * @dev Updates grace period duration. + * Callable only by the contract owner / proxy admin. + * @param _gracePeriod new grace period duration. + */ + function setGracePeriod(uint64 _gracePeriod) external onlyOwner { + gracePeriod = _gracePeriod; + emit UpdateGracePeriod(_gracePeriod); + } + + /** + * @dev Updates minimal fee required to be reserved for the prover who will submit the tree update proof. + * Callable only by the contract owner / proxy admin. + * @param _minTreeUpdateFee new minimal fee. + */ + function setMinTreeUpdateFee(uint64 _minTreeUpdateFee) external onlyOwner { + require(_minTreeUpdateFee > 0, "ZkBobPool: tree update fee can't be zero"); + minTreeUpdateFee = _minTreeUpdateFee; + emit UpdateMinTreeUpdateFee(_minTreeUpdateFee); + } + function _root() internal view override returns (uint256) { return roots[_transfer_index()]; } @@ -201,7 +262,9 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex * 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 { + function transactV2() external onlyOperator { + require(_version() == 2, "ZkBobPool: incorrect calldata version"); + address user = msg.sender; uint256 txType = _tx_type(); if (txType == 0) { @@ -226,25 +289,34 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex uint256 nullifier = _transfer_nullifier(); { + require(msg.sender == _memo_proxy_address(), "ZkBobPool: unauthorized"); require(nullifiers[nullifier] == 0, "ZkBobPool: doublespend detected"); require(_transfer_index() <= poolIndex, "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[poolIndex]), _tree_proof()), "ZkBobPool: bad tree proof"); + + _appendCommitment(_transfer_out_commit(), uint64(_memo_tree_update_fee()), _memo_prover_address()); nullifiers[nullifier] = uint256(keccak256(abi.encodePacked(_transfer_out_commit(), _transfer_delta()))); - poolIndex += 128; - roots[poolIndex] = _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"); + require(_isValidPrefix(bytes4(message) & 0x0000ffff), "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; - pool_index = poolIndex; - emit Message(poolIndex, _all_messages_hash, message); + + // Since there is no way to skip commitments in the queue + // we can precompute the index of this commitment in the merkle tree + uint256 pendingIndex = poolIndex + 128 * pendingCommitments.getSize(); + emit Message(pendingIndex, _all_messages_hash, message); } - uint256 fee = _memo_fee(); + uint256 transactFee = _memo_transact_fee(); + uint256 treeUpdateFee = _memo_tree_update_fee(); + + require(treeUpdateFee >= minTreeUpdateFee, "ZkBobPool: tree update fee is too low"); + + uint256 fee = transactFee + treeUpdateFee; + int256 token_amount = transfer_token_delta + int256(fee); int256 energy_amount = _transfer_energy_amount(); @@ -286,25 +358,23 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex } if (fee > 0) { - accumulatedFee[msg.sender] += fee; + accumulatedFee[msg.sender] += transactFee; } } /** * @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. + * @param _prover address of the privileged prover */ function appendDirectDeposits( - uint256 _root_after, uint256[] calldata _indices, uint256 _out_commit, uint256[8] memory _batch_deposit_proof, - uint256[8] memory _tree_proof + address _prover ) external onlyOperator @@ -322,131 +392,55 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex batch_deposit_verifier.verifyProof([hashsum], _batch_deposit_proof), "ZkBobPool: bad batch deposit proof" ); - uint256[3] memory tree_pub = [roots[poolIndex], _root_after, _out_commit]; - require(tree_verifier.verifyProof(tree_pub, _tree_proof), "ZkBobPool: bad tree proof"); + // we reserve the minimal tree update fee for the prover who will submit the tree update proof + require(totalFee >= minTreeUpdateFee, "ZkBobPool: tree update fee is too low"); + uint64 ddFee = uint64(totalFee) - minTreeUpdateFee; + + _appendCommitment(_out_commit, minTreeUpdateFee, _prover); - poolIndex += 128; - roots[poolIndex] = _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; - pool_index = poolIndex; - if (totalFee > 0) { - accumulatedFee[msg.sender] += totalFee; + if (ddFee > 0) { + accumulatedFee[msg.sender] += ddFee; } - emit Message(poolIndex, _all_messages_hash, message); - } - - /** - * @dev Commits a forced withdrawal transaction for future execution after a set delay. - * Forced exits can be executed during 23 hours after 1 hour passed since its commitment. - * Account cannot be recovered after such forced exit. - * any remaining or newly sent funds would be lost forever. - * Accumulated account energy is forfeited. - * @param _operator address that is allowed to call executeForcedExit, or address(0) if permissionless. - * @param _to withdrawn funds receiver. - * @param _amount total account balance to withdraw. - * @param _index index of the merkle root used within proof. - * @param _nullifier transfer nullifier to be used for withdrawal. - * @param _out_commit out commitment for empty list of output notes. - * @param _transfer_proof snark proof for transfer verifier. - */ - function commitForcedExit( - address _operator, - address _to, - uint256 _amount, - uint256 _index, - uint256 _nullifier, - uint256 _out_commit, - uint256[8] memory _transfer_proof - ) - external - { - require( - _amount > 0 && _amount % TOKEN_NUMERATOR == 0 && _amount <= 1 << 63, "ZkBobPool: incorrect token amount" - ); - require(_index < type(uint48).max, "ZkBobPool: index too large"); - - uint256 root = roots[_index]; - require(root > 0, "ZkBobPool: transfer index out of bounds"); - require(nullifiers[_nullifier] == 0, "ZkBobPool: doublespend detected"); - require(committedForcedExits[_nullifier] == 0, "ZkBobPool: already exists"); - - uint256[5] memory transfer_pub = [ - root, - _nullifier, - _out_commit, - (pool_id << 224) + (_index << 176) + uint64(-int64(uint64(_amount))), - uint256(keccak256(abi.encodePacked(_to))) % R - ]; - require(transfer_verifier.verifyProof(transfer_pub, _transfer_proof), "ZkBobPool: bad transfer proof"); - - committedForcedExits[_nullifier] = _hashForcedExit( - _operator, _to, _amount, block.timestamp + FORCED_EXIT_MIN_DELAY, block.timestamp + FORCED_EXIT_MAX_DELAY - ); - - emit CommitForcedExit( - _nullifier, - _operator, - _to, - _amount, - block.timestamp + FORCED_EXIT_MIN_DELAY, - block.timestamp + FORCED_EXIT_MAX_DELAY - ); + // Since there is no way to skip commitments in the queue + // we can precompute the index of this commitment in the merkle tree + uint256 pendingIndex = poolIndex + 128 * pendingCommitments.getSize(); + emit Message(pendingIndex, _all_messages_hash, message); } /** - * @dev Performs a forced withdrawal by irreversibly killing an account. - * Callable only by the operator, if set during latest call to the commitForcedExit. - * Account cannot be recovered after such forced exit. - * any remaining or newly sent funds would be lost forever. - * Accumulated account energy is forfeited. - * @param _nullifier transfer nullifier to be used for withdrawal. - * @param _operator operator address set during commitForcedExit. - * @param _to withdrawn funds receiver. - * @param _amount total account balance to withdraw. - * @param _exitStart exit window start timestamp, should match one calculated in commitForcedExit. - * @param _exitEnd exit window end timestamp, should match one calculated in commitForcedExit. - * @param _cancel cancel a previously submitted expired forced exit instead of executing it. + * @dev Updates pool index and merkle tree root if the provided proof is valid and + * the proof corresponds to the pending commitment. + * A prover specified in the pending commitment has a grace period to submit the tree update proof. + * @param _commitment pending commitment to be proven. + * @param _proof snark proof for tree update verifier. + * @param _rootAfter new merkle tree root. */ - function executeForcedExit( - uint256 _nullifier, - address _operator, - address _to, - uint256 _amount, - uint256 _exitStart, - uint256 _exitEnd, - bool _cancel + function proveTreeUpdate( + uint256 _commitment, + uint256[8] calldata _proof, + uint256 _rootAfter ) external + onlyOperator { - require(nullifiers[_nullifier] == 0, "ZkBobPool: doublespend detected"); - require( - committedForcedExits[_nullifier] == _hashForcedExit(_operator, _to, _amount, _exitStart, _exitEnd), - "ZkBobPool: invalid forced exit" - ); - if (_cancel) { - require(block.timestamp >= _exitEnd, "ZkBobPool: exit not expired"); - delete committedForcedExits[_nullifier]; - - emit CancelForcedExit(_nullifier); - return; - } + PendingCommitment memory commitment = pendingCommitments.popFront(); + require(commitment.commitment == _commitment, "ZkBobPool: commitment mismatch"); - require(_operator == address(0) || _operator == msg.sender, "ZkBobPool: invalid caller"); - require(block.timestamp >= _exitStart && block.timestamp < _exitEnd, "ZkBobPool: exit not allowed"); + _validateGracePeriod(commitment.timestamp, commitment.prover); - (IZkBobAccounting acc, uint96 poolIndex) = (accounting, pool_index); - if (address(acc) != address(0)) { - acc.recordOperation(IZkBobAccounting.TxType.ForcedExit, address(0), int256(_amount)); - } - nullifiers[_nullifier] = poolIndex | uint256(0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000); + uint256[3] memory tree_pub = [roots[pool_index], _rootAfter, _commitment]; + require(tree_verifier.verifyProof(tree_pub, _proof), "ZkBobPool: bad tree proof"); - IERC20(token).safeTransfer(_to, _amount * TOKEN_DENOMINATOR / TOKEN_NUMERATOR); + pool_index += 128; + roots[pool_index] = _rootAfter; + accumulatedFee[msg.sender] += commitment.fee; - emit ForcedExit(poolIndex, _nullifier, _to, _amount); + emit RootUpdated(pool_index, _rootAfter, _commitment); } /** @@ -481,29 +475,6 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex emit WithdrawFee(_operator, fee); } - /** - * @dev Calculates forced exit operation hash. - * @param _operator operator address. - * @param _to withdrawn funds receiver. - * @param _amount total account balance to withdraw. - * @param _exitStart exit window start timestamp, should match one calculated in commitForcedExit. - * @param _exitEnd exit window end timestamp, should match one calculated in commitForcedExit. - * @return operation hash. - */ - function _hashForcedExit( - address _operator, - address _to, - uint256 _amount, - uint256 _exitStart, - uint256 _exitEnd - ) - internal - pure - returns (bytes32) - { - return keccak256(abi.encode(_operator, _to, _amount, _exitStart, _exitEnd)); - } - /** * @dev Tells if caller is the contract owner. * Gives ownership rights to the proxy admin as well. @@ -512,4 +483,33 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex function _isOwner() internal view override returns (bool) { return super._isOwner() || _admin() == _msgSender(); } + + /** + * @dev Appends a commitment to the pending commitments queue. + */ + function _appendCommitment(uint256 _commitment, uint64 _fee, address _prover) internal { + pendingCommitments.pushBack( + PendingCommitment({commitment: _commitment, fee: _fee, prover: _prover, timestamp: uint32(block.timestamp)}) + ); + } + + /** + * @dev Validates that the prover is allowed to submit the tree update proof now. + */ + function _validateGracePeriod(uint64 commitmentTimestamp, address privilegedProver) internal view { + require( + msg.sender == privilegedProver || privilegedProver == address(0) + || block.timestamp > commitmentTimestamp + gracePeriod, + "ZkBobPool: prover is not allowed to submit the proof yet" + ); + } + + /** + * @dev Tells if given message prefix is valid. + * @param _prefix prefix to check. + * @return true, if prefix is valid. + */ + function _isValidPrefix(bytes4 _prefix) internal pure returns (bool) { + return _prefix == MESSAGE_PREFIX_COMMON_V1 || _prefix == MESSAGE_PREFIX_COMMON_V2; + } } diff --git a/src/zkbob/manager/AllowListOperatorManager.sol b/src/zkbob/manager/AllowListOperatorManager.sol new file mode 100644 index 0000000..de1b0fb --- /dev/null +++ b/src/zkbob/manager/AllowListOperatorManager.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import {IOperatorManager} from "../../interfaces/IOperatorManager.sol"; +import {Ownable} from "../../utils/Ownable.sol"; + +/** + * @title AllowListOperatorManager + * @dev Implements an allow list based access control for ZkBobPool relayers. + */ +contract AllowListOperatorManager is IOperatorManager, Ownable { + /** + * @param allowed flag to enable or disable operator. + * @param feeReceiver address of the fee receiver. + * @dev feeReceiver is still active even if allowed is false. + */ + struct Operator { + bool allowed; + address feeReceiver; + } + + // if true, only whitelisted addresses can be operators + // if false, anyone can be an operator + bool public allowListEnabled; + + // mapping of whitelisted operator addresses + mapping(address => Operator) public operators; + + event UpdateOperator(address indexed operator, address feeReceiver, bool allowed); + event UpdateAllowListEnabled(bool enabled); + + modifier nonZeroAddress(address addr) { + require(addr != address(0), "OperatorManager: zero address"); + _; + } + + constructor(address[] memory _operators, address[] memory _feeReceivers, bool _allowListEnabled) Ownable() { + require(_operators.length == _feeReceivers.length, "OperatorManager: arrays length mismatch"); + + allowListEnabled = _allowListEnabled; + for (uint256 i = 0; i < _operators.length; i++) { + _setOperator(_operators[i], true, _feeReceivers[i]); + } + } + + /** + * @dev Doesn't return any data, as operator URI is not used in this implementation. + */ + function operatorURI() external pure returns (string memory) { + return ""; + } + + /** + * @dev Sets the allow list enabled flag. + * @param _allowListEnabled flag to enable or disable allow list. + */ + function setAllowListEnabled(bool _allowListEnabled) external onlyOwner { + allowListEnabled = _allowListEnabled; + emit UpdateAllowListEnabled(_allowListEnabled); + } + + /** + * @dev Adds or removes an operator from the allow list. + * @param _operator address of the operator. + * @param _allowed flag to enable or disable operator. + * @param _feeReceiver address of the fee receiver. + */ + function setOperator(address _operator, address _feeReceiver, bool _allowed) external onlyOwner { + _setOperator(_operator, _allowed, _feeReceiver); + } + + /** + * @dev Adds or removes operators from the allow list. + * @param _operators addresses of the operators. + * @param _allowed flags to enable or disable operators. + * @param _feeReceivers addresses of the fee receivers. + */ + function setOperators( + address[] calldata _operators, + bool[] calldata _allowed, + address[] calldata _feeReceivers + ) + external + onlyOwner + { + require(_operators.length == _feeReceivers.length, "OperatorManager: arrays length mismatch"); + require(_operators.length == _allowed.length, "OperatorManager: arrays length mismatch"); + + for (uint256 i = 0; i < _operators.length; i++) { + _setOperator(_operators[i], _allowed[i], _feeReceivers[i]); + } + } + + /** + * @dev Sets the fee receiver for the operator. + * @param _feeReceiver address of the fee receiver. + */ + function setFeeReceiver(address _feeReceiver) external { + require(isOperator(msg.sender), "OperatorManager: operator not allowed"); + operators[msg.sender].feeReceiver = _feeReceiver; + emit UpdateOperator(msg.sender, _feeReceiver, true); + } + + function _setOperator(address _operator, bool _allowed, address _feeReceiver) internal nonZeroAddress(_operator) { + operators[_operator].allowed = _allowed; + if (_allowed) { + operators[_operator].feeReceiver = _feeReceiver; + } + emit UpdateOperator(_operator, operators[_operator].feeReceiver, _allowed); + } + + /** + * @dev Returns true if the address is an operator. + * @param _addr address to check. + */ + function isOperator(address _addr) public view override returns (bool) { + return operators[_addr].allowed || !allowListEnabled; + } + + /** + * @dev Returns true if the address is an operator fee receiver. + * @param _operator address of the operator. + * @param _addr address to check. + */ + function isOperatorFeeReceiver(address _operator, address _addr) external view override returns (bool) { + return operators[_operator].feeReceiver == _addr; + } +} diff --git a/src/zkbob/utils/CustomABIDecoder.sol b/src/zkbob/utils/CustomABIDecoder.sol index 35a242d..735c680 100644 --- a/src/zkbob/utils/CustomABIDecoder.sol +++ b/src/zkbob/utils/CustomABIDecoder.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.15; contract CustomABIDecoder { - uint256 constant transfer_nullifier_pos = 4; - uint256 constant transfer_nullifier_size = 32; uint256 constant uint256_size = 32; function _loaduint256(uint256 pos) internal pure returns (uint256 r) { @@ -13,6 +11,16 @@ contract CustomABIDecoder { } } + uint256 constant version_pos = 4; + uint256 constant version_size = 1; + + function _version() internal pure returns (uint8 r) { + r = uint8(_loaduint256(version_pos) >> (8 * (uint256_size - version_size))); + } + + uint256 constant transfer_nullifier_pos = version_pos + version_size; + uint256 constant transfer_nullifier_size = 32; + function _transfer_nullifier() internal pure returns (uint256 r) { r = _loaduint256(transfer_nullifier_pos); } @@ -55,24 +63,7 @@ contract CustomABIDecoder { } } - uint256 constant tree_root_after_pos = transfer_proof_pos + transfer_proof_size; - uint256 constant tree_root_after_size = 32; - - function _tree_root_after() internal pure returns (uint256 r) { - r = _loaduint256(tree_root_after_pos); - } - - uint256 constant tree_proof_pos = tree_root_after_pos + tree_root_after_size; - uint256 constant tree_proof_size = 256; - - function _tree_proof() internal pure returns (uint256[8] calldata r) { - uint256 pos = tree_proof_pos; - assembly { - r := pos - } - } - - uint256 constant tx_type_pos = tree_proof_pos + tree_proof_size; + uint256 constant tx_type_pos = transfer_proof_pos + transfer_proof_size; uint256 constant tx_type_size = 2; uint256 constant tx_type_mask = (1 << (tx_type_size * 8)) - 1; @@ -124,43 +115,70 @@ contract CustomABIDecoder { function _memo_fixed_size() internal pure returns (uint256 r) { uint256 t = _tx_type(); if (t == 0 || t == 1) { - // fee - // 8 - r = 8; + // proxy address + prover address + transact fee + tree update fee + // 20 + 20 + 8 + 8 + r = 56; } else if (t == 2) { - // fee + native amount + recipient - // 8 + 8 + 20 - r = 36; + // proxy address + prover address + transact fee + tree update fee + native amount + recipient + // 20 + 20 + 8 + 8 + 8 + 20 + r = 84; } else if (t == 3) { - // fee + deadline + address - // 8 + 8 + 20 - r = 36; + // proxy address + prover address + transact fee + tree update fee + deadline + address + // 20 + 20 + 8 + 8 + 8 + 20 + r = 84; } else { revert(); } } + uint256 constant memo_message_size_size = 2; + uint256 constant memo_message_size_mask = (1 << (memo_message_size_size * 8)) - 1; + function _memo_message() internal pure returns (bytes calldata r) { uint256 memo_fixed_size = _memo_fixed_size(); uint256 offset = memo_data_pos + memo_fixed_size; - uint256 length = _memo_data_size() - memo_fixed_size; + uint256 length = _loaduint256(offset + memo_message_size_size - uint256_size) & memo_message_size_mask; + offset += memo_message_size_size; assembly { r.offset := offset r.length := length } } - uint256 constant memo_fee_pos = memo_data_pos; - uint256 constant memo_fee_size = 8; - uint256 constant memo_fee_mask = (1 << (memo_fee_size * 8)) - 1; + uint256 constant memo_proxy_address_pos = memo_data_pos; + uint256 constant memo_proxy_address_size = 20; + + function _memo_proxy_address() internal pure returns (address r) { + r = address(uint160(_loaduint256(memo_proxy_address_pos + memo_proxy_address_size - uint256_size))); + } + + uint256 constant memo_prover_address_pos = memo_proxy_address_pos + memo_proxy_address_size; + uint256 constant memo_prover_address_size = 20; + + function _memo_prover_address() internal pure returns (address r) { + r = address(uint160(_loaduint256(memo_prover_address_pos + memo_prover_address_size - uint256_size))); + } + + uint256 constant memo_transact_fee_pos = memo_prover_address_pos + memo_prover_address_size; + uint256 constant memo_transact_fee_size = 8; + uint256 constant memo_transact_fee_mask = (1 << (memo_transact_fee_size * 8)) - 1; + + function _memo_transact_fee() internal pure returns (uint256 r) { + r = _loaduint256(memo_transact_fee_pos + memo_transact_fee_size - uint256_size) & memo_transact_fee_mask; + } + + uint256 constant memo_tree_update_fee_pos = memo_transact_fee_pos + memo_transact_fee_size; + uint256 constant memo_tree_update_fee_size = 8; + uint256 constant memo_tree_update_fee_mask = (1 << (memo_tree_update_fee_size * 8)) - 1; - function _memo_fee() internal pure returns (uint256 r) { - r = _loaduint256(memo_fee_pos + memo_fee_size - uint256_size) & memo_fee_mask; + function _memo_tree_update_fee() internal pure returns (uint256 r) { + r = _loaduint256(memo_tree_update_fee_pos + memo_tree_update_fee_size - uint256_size) + & memo_tree_update_fee_mask; } // Withdraw specific data - uint256 constant memo_native_amount_pos = memo_fee_pos + memo_fee_size; + uint256 constant memo_native_amount_pos = memo_tree_update_fee_pos + memo_tree_update_fee_size; uint256 constant memo_native_amount_size = 8; uint256 constant memo_native_amount_mask = (1 << (memo_native_amount_size * 8)) - 1; @@ -177,7 +195,7 @@ contract CustomABIDecoder { // Permittable token deposit specific data - uint256 constant memo_permit_deadline_pos = memo_fee_pos + memo_fee_size; + uint256 constant memo_permit_deadline_pos = memo_tree_update_fee_pos + memo_tree_update_fee_size; uint256 constant memo_permit_deadline_size = 8; function _memo_permit_deadline() internal pure returns (uint64 r) { diff --git a/src/zkbob/utils/Parameters.sol b/src/zkbob/utils/Parameters.sol index a27f2fa..da56895 100644 --- a/src/zkbob/utils/Parameters.sol +++ b/src/zkbob/utils/Parameters.sol @@ -20,12 +20,6 @@ abstract contract Parameters is CustomABIDecoder { r[4] = uint256(keccak256(_memo_data())) % R; } - function _tree_pub(uint256 _root_before) internal view returns (uint256[3] memory r) { - r[0] = _root_before; - r[1] = _tree_root_after(); - r[2] = _transfer_out_commit(); - } - // NOTE only valid in the context of normal deposit (tx_type=0) function _deposit_spender() internal pure returns (address) { (bytes32 r, bytes32 vs) = _sign_r_vs(); diff --git a/src/zkbob/utils/Queue.sol b/src/zkbob/utils/Queue.sol new file mode 100644 index 0000000..daebe6e --- /dev/null +++ b/src/zkbob/utils/Queue.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2019 Matter Labs + +pragma solidity ^0.8.13; + +/** + * @dev The structure that stores all information about the pending commitment. + * @param commitment commitment value to be added in the Merkle Tree. + * @param prover address of the prover that submitted the commitment. + * @param fee fee reserved for the prover who will submit the tree update proof. + * @param timestamp commitment timestamp. + */ +struct PendingCommitment { + uint256 commitment; + address prover; + uint64 fee; + uint32 timestamp; +} + +/// @dev The library provides the API to interact with the priority queue container +/// @dev Order of processing operations from queue - FIFO (Fist in - first out) +library Queue { + using Queue for CommitmentQueue; + + /// @notice Container that stores pending commitments + /// @param data The inner mapping that saves pending commitment by its index + /// @param head The pointer to the first unprocessed pending commitment, equal to the tail if the queue is empty + /// @param tail The pointer to the free slot + struct CommitmentQueue { + mapping(uint256 => PendingCommitment) data; + uint256 tail; + uint256 head; + } + + /// @return The total number of unprocessed pending commitments in a priority queue + function getSize(CommitmentQueue storage _queue) internal view returns (uint256) { + return uint256(_queue.tail - _queue.head); + } + + /// @return Whether the priority queue contains no pending commitments + function isEmpty(CommitmentQueue storage _queue) internal view returns (bool) { + return _queue.tail == _queue.head; + } + + /// @notice Add the pending commitment to the end of the priority queue + function pushBack(CommitmentQueue storage _queue, PendingCommitment memory _commitment) internal { + // Save value into the stack to avoid double reading from the storage + uint256 tail = _queue.tail; + + _queue.data[tail] = _commitment; + _queue.tail = tail + 1; + } + + function list(CommitmentQueue storage _queue) internal view returns (PendingCommitment[] memory) { + PendingCommitment[] memory result = new PendingCommitment[](_queue.getSize()); + for (uint256 index = _queue.head; index < _queue.tail; index++) { + result[index - _queue.head] = _queue.data[index]; + } + return result; + } + + /// @return The first unprocessed pending commitment from the queue + function front(CommitmentQueue storage _queue) internal view returns (PendingCommitment memory) { + require(!_queue.isEmpty(), "ZkBobPool: queue is empty"); // priority queue is empty + + return _queue.data[_queue.head]; + } + + /// @notice Remove the first unprocessed pending commitment from the queue + /// @return pendingCommitment that was popped from the priority queue + function popFront(CommitmentQueue storage _queue) internal returns (PendingCommitment memory pendingCommitment) { + require(!_queue.isEmpty(), "ZkBobPool: queue is empty"); // priority queue is empty + + // Save value into the stack to avoid double reading from the storage + uint256 head = _queue.head; + + pendingCommitment = _queue.data[head]; + delete _queue.data[head]; + _queue.head = head + 1; + } +} diff --git a/src/zkbob/utils/ZkBobAccounting.sol b/src/zkbob/utils/ZkBobAccounting.sol index eeb2975..a7105f3 100644 --- a/src/zkbob/utils/ZkBobAccounting.sol +++ b/src/zkbob/utils/ZkBobAccounting.sol @@ -294,11 +294,6 @@ contract ZkBobAccounting is IZkBobAccounting, Ownable { _recordDirectDeposit(_user, uint256(_txAmount)); return; } - if (_txType == IZkBobAccounting.TxType.ForcedExit) { - require(_txAmount > 0, "ZkBobAccounting: negative amount"); - slot1.tvl -= uint72(uint256(_txAmount)); - return; - } Slot0 memory s0 = slot0; Slot1 memory s1 = slot1; diff --git a/test/interfaces/IZkBobPoolAdmin.sol b/test/interfaces/IZkBobPoolAdmin.sol index b215451..cf5989b 100644 --- a/test/interfaces/IZkBobPoolAdmin.sol +++ b/test/interfaces/IZkBobPoolAdmin.sol @@ -11,6 +11,8 @@ interface IZkBobPoolAdmin { function pool_index() external view returns (uint256); + function pendingCommitment() external view returns (uint256, address, uint64, uint64, uint64); + function initialize(uint256 _root) external; function setTokenSeller(address _tokenSeller) external; @@ -23,40 +25,21 @@ interface IZkBobPoolAdmin { function setEnergyRedeemer(IEnergyRedeemer _redeemer) external; - function accounting() external view returns (address); + function setGracePeriod(uint64 _gracePeriod) external; - function transact() external; + function setMinTreeUpdateFee(uint64 _minTreeUpdateFee) external; - function committedForcedExits(uint256 _nullifier) external view returns (bytes32); + function accounting() external view returns (address); - function commitForcedExit( - address _operator, - address _to, - uint256 _amount, - uint256 _index, - uint256 _nullifier, - uint256 _out_commit, - uint256[8] memory _transfer_proof - ) - external; + function transact() external; - function executeForcedExit( - uint256 _nullifier, - address _operator, - address _to, - uint256 _amount, - uint256 _exitStart, - uint256 _exitEnd, - bool _cancel - ) - external; + function proveTreeUpdate(uint256, uint256[8] memory, uint256) external; function appendDirectDeposits( - uint256 _root_after, uint256[] calldata _indices, uint256 _out_commit, uint256[8] memory _batch_deposit_proof, - uint256[8] memory _tree_proof + address _prover ) external; @@ -85,4 +68,6 @@ interface IZkBobPoolAdmin { function direct_deposit_queue() external view returns (address); function pool_id() external view returns (uint256); + + function gracePeriod() external view returns (uint64); } diff --git a/test/libraries/Queue.t.sol b/test/libraries/Queue.t.sol new file mode 100644 index 0000000..e8becd6 --- /dev/null +++ b/test/libraries/Queue.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.15; + +import "forge-std/Test.sol"; +import {Queue, PendingCommitment} from "../../src/zkbob/utils/Queue.sol"; +import "forge-std/console.sol"; + +contract PriorityQueueTest is Test { + address immutable prover1 = makeAddr("Prover #1"); + DummyQueue _queue; + + function setUp() external { + _queue = new DummyQueue(); + } + + function testEmptyQueue() external { + assertEq(_queue.getSize(), 0); + assertEq(_queue.isEmpty(), true); + + vm.expectRevert("ZkBobPool: queue is empty"); + _queue.popFront(); + + vm.expectRevert("ZkBobPool: queue is empty"); + _queue.front(); + + PendingCommitment[] memory ops = _queue.list(); + assertEq(0, ops.length); + } + + function testPushBackPopFront() external { + for (uint256 i = 0; i < 100; i++) { + _queue.pushBack(_newOp(i)); + + assertEq(i, _queue.head()); + assertEq(i + 1, _queue.tail()); + assertEq(1, _queue.getSize()); + + PendingCommitment memory commitment = _queue.front(); + _verifyOp(i, commitment); + + PendingCommitment[] memory ops = _queue.list(); + assertEq(1, ops.length); + _verifyOp(i, ops[0]); + + PendingCommitment memory popped = _queue.popFront(); + _verifyOp(i, popped); + } + } + + function _newOp(uint256 id) internal pure returns (PendingCommitment memory) { + address prover = address(uint160(uint256(keccak256(abi.encodePacked("prover", id))))); + uint64 fee = uint64(uint256(keccak256(abi.encodePacked("fee", id)))); + uint32 timestamp = uint32(uint256(keccak256(abi.encodePacked("timestamp", id)))); + return PendingCommitment(id, prover, fee, timestamp); + } + + function _verifyOp(uint256 id, PendingCommitment memory op) internal { + address prover = address(uint160(uint256(keccak256(abi.encodePacked("prover", id))))); + uint64 fee = uint64(uint256(keccak256(abi.encodePacked("fee", id)))); + uint32 timestamp = uint32(uint256(keccak256(abi.encodePacked("timestamp", id)))); + + assertEq(op.commitment, id); + assertEq(op.prover, prover); + assertEq(op.fee, fee); + assertEq(op.timestamp, timestamp); + } +} + +/** + * @dev Helper contract to test PriorityQueue library + * Without this contract forge coverage doesn't work properly + */ +contract DummyQueue { + Queue.CommitmentQueue _queue; + + function list() external view returns (PendingCommitment[] memory) { + return Queue.list(_queue); + } + + function pushBack(PendingCommitment memory _operation) external { + Queue.pushBack(_queue, _operation); + } + + function head() external view returns (uint256) { + return _queue.head; + } + + function tail() external view returns (uint256) { + return _queue.tail; + } + + function getSize() external view returns (uint256) { + return Queue.getSize(_queue); + } + + function isEmpty() external view returns (bool) { + return Queue.isEmpty(_queue); + } + + function front() external view returns (PendingCommitment memory) { + return Queue.front(_queue); + } + + function popFront() external returns (PendingCommitment memory pendingCommitments) { + return Queue.popFront(_queue); + } +} diff --git a/test/shared/Env.t.sol b/test/shared/Env.t.sol index 3a3e196..b2191cf 100644 --- a/test/shared/Env.t.sol +++ b/test/shared/Env.t.sol @@ -17,8 +17,8 @@ address constant bobVanityAddr = address(0xB0B195aEFA3650A6908f15CdaC7D92F8a5791 bytes32 constant bobSalt = bytes32(uint256(285834900769)); uint256 constant forkBlockMainnet = 16200000; -string constant forkRpcUrlMainnet = "https://rpc.ankr.com/eth"; +string constant forkRpcUrlMainnet = "https://eth.llamarpc.com"; uint256 constant forkBlockPolygon = 37000000; -string constant forkRpcUrlPolygon = "https://rpc.ankr.com/polygon"; +string constant forkRpcUrlPolygon = "https://polygon-rpc.com"; uint256 constant forkBlockOptimism = 52000000; -string constant forkRpcUrlOptimism = "https://1rpc.io/op"; +string constant forkRpcUrlOptimism = "https://optimism.llamarpc.com"; diff --git a/test/zkbob/ZkBobPool.t.sol b/test/zkbob/ZkBobPool.t.sol index 8428f9b..927c2c2 100644 --- a/test/zkbob/ZkBobPool.t.sol +++ b/test/zkbob/ZkBobPool.t.sol @@ -30,7 +30,7 @@ import "../../src/zkbob/ZkBobPoolETH.sol"; import "../../src/infra/UniswapV3Seller.sol"; import {EnergyRedeemer} from "../../src/infra/EnergyRedeemer.sol"; -abstract contract AbstractZkBobPoolTest is AbstractForkTest { +abstract contract AbstractZkBobPoolTestBase is AbstractForkTest { address constant permit2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant uniV3Quoter = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; @@ -86,7 +86,7 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { IOperatorManager operatorManager; ZkBobAccounting accounting; - function setUp() public { + function setUp() public virtual { vm.createSelectFork(forkRpcUrl, forkBlock); EIP1967Proxy poolProxy = new EIP1967Proxy(address(this), address(0xdead), ""); @@ -95,27 +95,42 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { ZkBobPool impl; if (poolType == PoolType.ETH) { impl = new ZkBobPoolETH( - 0, token, - new TransferVerifierMock(), new TreeUpdateVerifierMock(), new BatchDepositVerifierMock(), - address(queueProxy), permit2 + 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(), + 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(), + 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 + 0, + token, + new TransferVerifierMock(), + new TreeUpdateVerifierMock(), + new BatchDepositVerifierMock(), + address(queueProxy), + permit2, + 1_000_000_000 ); } @@ -150,16 +165,380 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { queue.setDirectDepositFee(uint64(0.1 ether / D)); queue.setDirectDepositTimeout(1 days); + pool.setGracePeriod(5 minutes); + pool.setMinTreeUpdateFee(1); + deal(token, user1, 1 ether / D); deal(token, user3, 0); } + function _setUpDD() internal { + deal(user1, 100 ether / D); + deal(user2, 100 ether / D); + deal(address(token), user1, 100 ether / D); + deal(address(token), user2, 100 ether / D); + + accounting.setLimits( + 1, + 2_000_000 ether / D / denominator, + 200_000 ether / D / denominator, + 200_000 ether / D / denominator, + 20_000 ether / D / denominator, + 20_000 ether / D / denominator, + 25 ether / D / denominator, + 10 ether / D / denominator + ); + address[] memory users = new address[](1); + users[0] = user1; + accounting.setUsersTier(1, users); + + 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 _encodeDeposit( + int256 _amount, + uint256 _transactFee, + uint256 _treeUpdateFee, + address _proxyAndProver + ) + internal + view + returns (bytes memory) + { + bytes32 nullifier = bytes32(_randFR()); + + bytes memory permitSignature; + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, ECDSA.toEthSignedMessageHash(nullifier)); + permitSignature = abi.encodePacked(r, uint256(s) + (v == 28 ? (1 << 255) : 0)); + } + bytes memory data = abi.encodePacked( + ZkBobPool.transactV2.selector, + uint8(2), + nullifier, + _randFR(), + uint48(0), + uint112(0), + int64(_amount / int256(denominator)) + ); + for (uint256 i = 0; i < 8; i++) { + data = abi.encodePacked(data, _randFR()); + } + data = abi.encodePacked( + data, + uint16(0), + uint16(104), + _proxyAndProver, + _proxyAndProver, + uint64(_transactFee / denominator), + uint64(_treeUpdateFee / denominator), + _memoMessageAndExtraData(bytes2(0)) + ); + return abi.encodePacked(data, permitSignature); + } + + function _encodeWithdrawal( + address _to, + uint256 _amount, + uint256 _nativeAmount, + uint256 _energyAmount, + address _proxyAndProver + ) + internal + view + returns (bytes memory) + { + bytes memory data = abi.encodePacked( + ZkBobPool.transactV2.selector, + uint8(2), + _randFR(), + _randFR(), + uint48(0), + -int112(int256(_energyAmount)), + int64(-int256((_amount / denominator) + 0.005 ether / D / denominator + 0.005 ether / D / denominator)) + ); + for (uint256 i = 0; i < 8; i++) { + data = abi.encodePacked(data, _randFR()); + } + + data = abi.encodePacked(data, uint16(2), uint16(132)); + + return abi.encodePacked( + data, + _proxyAndProver, + _proxyAndProver, + uint64(0.005 ether / D / denominator), + uint64(0.005 ether / D / denominator), + uint64(_nativeAmount / denominator), + _to, + _memoMessageAndExtraData(bytes2(0)) + ); + } + + function _encodeTransfer( + uint256 _transactFee, + uint256 _treeUpdateFee, + address _proxyAndProver + ) + internal + view + returns (bytes memory) + { + return _encodeTransferWithPrefix(_transactFee, _treeUpdateFee, _proxyAndProver, bytes2(0)); + } + + function _encodeTransferWithPrefix( + uint256 _transactFee, + uint256 _treeUpdateFee, + address _proxyAndProver, + bytes2 _prefix + ) + internal + view + returns (bytes memory) + { + bytes memory data = abi.encodePacked( + ZkBobPool.transactV2.selector, + uint8(2), + _randFR(), + _randFR(), + uint48(0), + uint112(0), + -int64(uint64((_transactFee + _treeUpdateFee) / denominator)) + ); + for (uint256 i = 0; i < 8; i++) { + data = abi.encodePacked(data, _randFR()); + } + return abi.encodePacked( + data, + uint16(1), + uint16(104), + _proxyAndProver, + _proxyAndProver, + uint64(_transactFee / denominator), + uint64(_treeUpdateFee / denominator), + _memoMessageAndExtraData(_prefix) + ); + } + + function _memoMessageAndExtraData(bytes2 _prefix) internal view returns (bytes memory) { + return abi.encodePacked( + uint16(36), // memo message size + bytes2(0x0100), + _prefix, + _randFR(), + bytes("extra data") + ); + } + + function _transact(bytes memory _data) internal { + vm.prank(user2); + (bool status,) = address(pool).call(_data); + require(status, "transact() reverted"); + } + + function _proveTreeUpdate() internal { + vm.startPrank(user2); + (uint256 commitment,,,,) = pool.pendingCommitment(); + pool.proveTreeUpdate(commitment, _randProof(), _randFR()); + vm.stopPrank(); + } + + 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 view returns (uint256) { + return uint256(keccak256(abi.encode(gasleft()))) + % 21888242871839275222246405745257275088696311157297823662689037894645226208583; + } + + function _randProof() internal view returns (uint256[8] memory) { + return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; + } + + function _encodePermitDeposit( + int256 _amount, + uint256 _transactFee, + uint256 _treeUpdateFee, + address _proxyAndProver + ) + internal + returns (bytes memory) + { + return _encodePermitDeposit(_amount, _transactFee, _treeUpdateFee, _proxyAndProver, _proxyAndProver); + } + + function _encodePermitDeposit( + int256 _amount, + uint256 _transactFee, + uint256 _treeUpdateFee, + address _proxy, + address _prover + ) + 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()); + + bytes memory signature; + { + bytes32 digest; + if (permitType == PermitType.BOBPermit) { + digest = _digestSaltedPermit( + user1, + address(pool), + uint256(_amount + int256(_transactFee) + int256(_treeUpdateFee)), + expiry, + nullifier + ); + } else if (permitType == PermitType.Permit2) { + digest = _digestPermit2( + user1, + address(pool), + uint256(_amount + int256(_transactFee) + int256(_treeUpdateFee)), + expiry, + nullifier + ); + } else if (permitType == PermitType.USDCPermit) { + digest = _digestUSDCPermit( + user1, + address(pool), + uint256(_amount + int256(_transactFee) + int256(_treeUpdateFee)), + expiry, + nullifier + ); + } + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk1, digest); + signature = abi.encodePacked(r, uint256(s) + (v == 28 ? (1 << 255) : 0)); + } + + bytes memory data = abi.encodePacked(ZkBobPool.transactV2.selector, uint8(2)); + + data = abi.encodePacked(data, nullifier, _randFR(), uint48(0), uint112(0), int64(_amount / int256(denominator))); + for (uint256 i = 0; i < 8; i++) { + data = abi.encodePacked(data, _randFR()); + } + + data = abi.encodePacked(data, uint16(3), uint16(132)); + + data = abi.encodePacked( + data, + _proxy, + _prover, + uint64(_transactFee / denominator), + uint64(_treeUpdateFee / denominator), + uint64(expiry), + user1, + _memoMessageAndExtraData(bytes2(0)) + ); + return abi.encodePacked(data, signature); + } + + 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 _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); + } + } +} + +abstract contract AbstractZkBobPoolTest is AbstractZkBobPoolTestBase { + function setUp() public override { + super.setUp(); + } + function testSimpleTransaction() public { - bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); - bytes memory data2 = _encodeTransfer(0.01 ether / D); + bytes memory data2 = _encodeTransfer(0.005 ether / D, 0.005 ether / D, user2); _transact(data2); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -170,13 +549,15 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { assertEq(pool.pool_index(), 0); assertEq(pool.denominator(), denominator); - bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); assertEq(pool.pool_index(), 128); - bytes memory data2 = _encodeTransfer(0.01 ether / D); + bytes memory data2 = _encodeTransfer(0.005 ether / D, 0.005 ether / D, user2); _transact(data2); + _proveTreeUpdate(); assertEq(pool.pool_index(), 256); } @@ -226,11 +607,13 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { function testResetDailyLimits() public { deal(token, user1, 10 ether / D); - bytes memory data1 = _encodePermitDeposit(int256(5 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); - bytes memory data2 = _encodeWithdrawal(user1, 4 ether / D, 0, 0); + bytes memory data2 = _encodeWithdrawal(user1, 4 ether / D, 0, 0, user2); _transact(data2); + _proveTreeUpdate(); assertEq(accounting.getLimitsFor(user1).dailyDepositCapUsage, 5 ether / D / denominator); assertEq(accounting.getLimitsFor(user1).dailyWithdrawalCapUsage, 4.01 ether / D / denominator); @@ -251,8 +634,9 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { } function testPermitDeposit() public { - bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); + bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -264,12 +648,16 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { 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); + bytes memory data = _encodePermitDeposit( + int256(0.1 ether / D) * int256(i), 0.0005 ether / D * i, 0.0005 ether / D * i, user2 + ); _transact(data); + _proveTreeUpdate(); } - bytes memory data2 = _encodeTransfer(0.01 ether / D); + bytes memory data2 = _encodeTransfer(0.005 ether / D, 0.005 ether / D, user2); _transact(data2); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -280,8 +668,9 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { vm.prank(user1); IERC20(token).approve(address(pool), 0.51 ether / D); - bytes memory data = _encodeDeposit(int256(0.5 ether / D), 0.01 ether / D); + bytes memory data = _encodeDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -291,11 +680,13 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { } function testWithdrawal() public { - bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); - bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether / D, 0, 0); + bytes memory data2 = _encodeWithdrawal(user1, 0.1 ether / D, 0, 0, user2); _transact(data2); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -304,127 +695,21 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { assertEq(IERC20(token).balanceOf(user3), 0.02 ether / D); } - function testForcedExit() public { - bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); - _transact(data); - - uint256 nullifier = _randFR(); - pool.commitForcedExit(user2, user2, 0.4 ether / D / denominator, 128, nullifier, _randFR(), _randProof()); - uint256 exitStart = block.timestamp + 1 hours; - uint256 exitEnd = block.timestamp + 24 hours; - - assertEq(IERC20(token).balanceOf(user2), 0); - assertEq(pool.nullifiers(nullifier), 0); - - vm.expectRevert("ZkBobPool: invalid forced exit"); - pool.executeForcedExit(nullifier ^ 1, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, false); - - vm.expectRevert("ZkBobPool: invalid forced exit"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, block.timestamp, exitEnd, false); - - vm.expectRevert("ZkBobPool: invalid caller"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, false); - - vm.startPrank(user2); - vm.expectRevert("ZkBobPool: exit not allowed"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, false); - - skip(25 hours); - vm.expectRevert("ZkBobPool: exit not allowed"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, false); - - rewind(23 hours); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, false); - - vm.expectRevert("ZkBobPool: doublespend detected"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, false); - - assertEq(IERC20(token).balanceOf(user2), 0.4 ether / D); - assertEq(pool.nullifiers(nullifier), 0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000080); - - vm.expectRevert("ZkBobPool: doublespend detected"); - pool.commitForcedExit(user2, user2, 0.4 ether / D / denominator, 128, nullifier, _randFR(), _randProof()); - - vm.stopPrank(); - } - - function testCancelForcedExit() public { - bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.01 ether / D); - _transact(data); - - uint256 nullifier = _randFR(); - pool.commitForcedExit(user2, user2, 0.4 ether / D / denominator, 128, nullifier, _randFR(), _randProof()); - uint256 exitStart = block.timestamp + 1 hours; - uint256 exitEnd = block.timestamp + 24 hours; - bytes32 hash = pool.committedForcedExits(nullifier); - assertNotEq(hash, bytes32(0)); - - vm.expectRevert("ZkBobPool: exit not expired"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, true); - vm.expectRevert("ZkBobPool: already exists"); - pool.commitForcedExit(user2, user2, 0.4 ether / D / denominator, 128, nullifier, _randFR(), _randProof()); - - skip(12 hours); - - vm.expectRevert("ZkBobPool: exit not expired"); - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, true); - vm.expectRevert("ZkBobPool: already exists"); - pool.commitForcedExit(user2, user2, 0.4 ether / D / denominator, 128, nullifier, _randFR(), _randProof()); - - skip(24 hours); - - pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd, true); - assertEq(pool.committedForcedExits(nullifier), bytes32(0)); - - pool.commitForcedExit(user2, user2, 0.4 ether / D / denominator, 128, nullifier, _randFR(), _randProof()); - assertNotEq(pool.committedForcedExits(nullifier), bytes32(0)); - assertNotEq(pool.committedForcedExits(nullifier), hash); - } - function testRejectNegativeDeposits() public { - bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); - bytes memory data2 = _encodePermitDeposit(-int256(0.5 ether / D), 1 ether / D); + bytes memory data2 = _encodePermitDeposit(-int256(0.5 ether / D), 0.5 ether / D, 0.5 ether / D, user2); _transactReverted(data2, "ZkBobPool: incorrect deposit amounts"); vm.prank(user1); IERC20(token).approve(address(pool), 0.5 ether / D); - bytes memory data3 = _encodeDeposit(-int256(0.5 ether / D), 1 ether / D); + bytes memory data3 = _encodeDeposit(-int256(0.5 ether / D), 0.5 ether / D, 0.5 ether / D, user2); _transactReverted(data3, "ZkBobPool: incorrect deposit amounts"); } - function _setUpDD() internal { - deal(user1, 100 ether / D); - deal(user2, 100 ether / D); - deal(address(token), user1, 100 ether / D); - deal(address(token), user2, 100 ether / D); - - accounting.setLimits( - 1, - 2_000_000 ether / D / denominator, - 200_000 ether / D / denominator, - 200_000 ether / D / denominator, - 20_000 ether / D / denominator, - 20_000 ether / D / denominator, - 25 ether / D / denominator, - 10 ether / D / denominator - ); - address[] memory users = new address[](1); - users[0] = user1; - accounting.setUsersTier(1, users); - - 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(); @@ -517,7 +802,7 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { vm.expectEmit(true, false, false, true); emit Message(128, bytes32(0), message); vm.prank(user2); - pool.appendDirectDeposits(_randFR(), indices, outCommitment, _randProof(), _randProof()); + pool.appendDirectDeposits(indices, outCommitment, _randProof(), address(0)); } function testRefundDirectDeposit() public { @@ -580,16 +865,18 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { deal(address(token), address(user1), 10 ether / D); - bytes memory data = _encodePermitDeposit(int256(4 ether / D), 0.01 ether / D); + bytes memory data = _encodePermitDeposit(int256(4 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data); + _proveTreeUpdate(); - bytes memory data2 = _encodeWithdrawal(user1, 1 ether / D, 0, 0); + bytes memory data2 = _encodeWithdrawal(user1, 1 ether / D, 0, 0, user2); _transact(data2); + _proveTreeUpdate(); - bytes memory data3 = _encodePermitDeposit(int256(3 ether / D), 0.01 ether / D); + bytes memory data3 = _encodePermitDeposit(int256(3 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transactReverted(data3, "ZkBobAccounting: daily user deposit cap exceeded"); - bytes memory data4 = _encodeWithdrawal(user1, 2 ether / D, 0, 0); + bytes memory data4 = _encodeWithdrawal(user1, 2 ether / D, 0, 0, user2); _transactReverted(data4, "ZkBobAccounting: daily withdrawal cap exceeded"); assertEq(accounting.getLimitsFor(user1).dailyUserDepositCapUsage, 4 ether / D / denominator); @@ -613,21 +900,23 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { 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); + assertEq(address(uint160(uint256(vm.load(address(pool), bytes32(uint256(66)))))), addr); } vm.deal(user1, 0); - bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); // 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, 0); + bytes memory data2 = _encodeWithdrawal(user1, 0.4 ether / D, 0.3 ether / D, 0, user2); _transact(data2); + _proveTreeUpdate(); // 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, 0); + bytes memory data4 = _encodeWithdrawal(user1, 0.2 ether / D, 0.3 ether / D, 0, user2); vm.prank(user2); (bool status, bytes memory returnData) = address(pool).call(data4); assert(!status); @@ -635,8 +924,9 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { 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, 0); + bytes memory data3 = _encodeWithdrawal(dummy, 0.4 ether / D, 0.3 ether / D, 0, user2); _transact(data3); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -670,8 +960,9 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { 0, 0 ); - bytes memory data1 = _encodePermitDeposit(int256(250_000 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(250_000 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); accounting.setLimits( 0, 1_000_000 ether / D / denominator, @@ -682,11 +973,12 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { 0, 0 ); - bytes memory data2 = _encodeTransfer(200_000 ether / D); + bytes memory data2 = _encodeTransfer(100_000 ether / D, 100_000 ether / D, user2); _transactReverted(data2, "ZkBobAccounting: daily withdrawal cap exceeded"); - bytes memory data3 = _encodeTransfer(20_000 ether / D); + bytes memory data3 = _encodeTransfer(10_000 ether / D, 10_000 ether / D, user2); _transact(data3); + _proveTreeUpdate(); vm.prank(user3); pool.withdrawFee(user2, user3); @@ -698,8 +990,9 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { deal(token, user1, 2_000_000 ether / D); for (uint256 i = 0; i < 100; i++) { - bytes memory data1 = _encodePermitDeposit(int256(2_500 ether / D), 0.01 ether / D); + bytes memory data1 = _encodePermitDeposit(int256(2_500 ether / D), 0.005 ether / D, 0.005 ether / D, user2); _transact(data1); + _proveTreeUpdate(); skip(6 hours + 1); } @@ -710,8 +1003,9 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { pool.setEnergyRedeemer(redeemer); // 1e18 energy ~= account balance of 100k BOB across 10k tx indices - bytes memory data2 = _encodeWithdrawal(user1, 0, 0, 1e18); + bytes memory data2 = _encodeWithdrawal(user1, 0, 0, 1e18, user2); _transact(data2); + _proveTreeUpdate(); // max weekly tvl ~= 200k // max weekly tx count ~= 28 @@ -719,203 +1013,62 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { assertApproxEqAbs(rewardToken.balanceOf(user1), 1785 ether, 200 ether); } - 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 / int256(denominator)) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _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)); - } - - function _encodeWithdrawal( - address _to, - uint256 _amount, - uint256 _nativeAmount, - uint256 _energyAmount - ) - internal - returns (bytes memory) - { - bytes memory data = abi.encodePacked( - ZkBobPool.transact.selector, - _randFR(), - _randFR(), - uint48(0), - -int112(int256(_energyAmount)), - int64(-int256((_amount / denominator) + 0.01 ether / D / denominator)) - ); - for (uint256 i = 0; i < 17; i++) { - data = abi.encodePacked(data, _randFR()); - } - return abi.encodePacked( - data, - uint16(2), - uint16(72), - uint64(0.01 ether / D / denominator), - uint64(_nativeAmount / denominator), - _to, - bytes4(0x01000000), - _randFR() - ); - } - - function _encodeTransfer(uint256 _fee) internal returns (bytes memory) { - bytes memory data = abi.encodePacked( - 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(_fee / denominator), bytes4(0x01000000), _randFR()); - } + function testTransactMessageEvent() public { + bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); + bytes memory message = _slice(data, 443, 36); + vm.expectEmit(true, false, false, true); + emit Message(128, bytes32(0), message); + _transact(data); + _proveTreeUpdate(); - function _transact(bytes memory _data) internal { - vm.prank(user2); - (bool status,) = address(pool).call(_data); - require(status, "transact() reverted"); - } + vm.prank(user1); + IERC20(token).approve(address(pool), 0.11 ether / D); - 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)); - } + bytes memory data1 = _encodeDeposit(int256(0.1 ether / D), 0.005 ether / D, 0.005 ether / D, user2); + bytes memory message1 = _slice(data1, 415, 36); + vm.expectEmit(true, false, false, true); + emit Message(256, bytes32(0), message1); + _transact(data1); + _proveTreeUpdate(); - function _randFR() internal returns (uint256) { - return uint256(keccak256(abi.encode(gasleft()))) - % 21888242871839275222246405745257275088696311157297823662689037894645226208583; - } + bytes memory data2 = _encodeTransfer(0.005 ether / D, 0.005 ether / D, user2); + bytes memory message2 = _slice(data2, 415, 36); + vm.expectEmit(true, false, false, true); + emit Message(384, bytes32(0), message2); + _transact(data2); + _proveTreeUpdate(); - function _randProof() internal returns (uint256[8] memory) { - return [_randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR(), _randFR()]; + bytes memory data3 = _encodeWithdrawal(user1, 0.1 ether / D, 0, 0, user2); + bytes memory message3 = _slice(data3, 443, 36); + vm.expectEmit(true, false, false, true); + emit Message(512, bytes32(0), message3); + _transact(data3); + _proveTreeUpdate(); } - 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); + function testTransactAcceptsOnlyValidPrefixes() public { + bytes memory data = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, user2); + _transact(data); - 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)); - } + data = _encodeTransferWithPrefix(0.005 ether / D, 0.005 ether / D, user2, bytes2(0x0000)); + _transact(data); - 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 - ) - ) - ); - } + data = _encodeTransferWithPrefix(0.005 ether / D, 0.005 ether / D, user2, bytes2(0x0002)); + _transact(data); - 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 - ) - ) - ); - } + data = _encodeTransferWithPrefix(0.005 ether / D, 0.005 ether / D, user2, bytes2(0x0001)); + _transactReverted(data, "ZkBobPool: bad message prefix"); - 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)) - ); + data = _encodeTransferWithPrefix(0.005 ether / D, 0.005 ether / D, user2, bytes2(0x1234)); + _transactReverted(data, "ZkBobPool: bad message prefix"); } - 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); + function _slice(bytes memory data, uint256 start, uint256 length) internal pure returns (bytes memory) { + bytes memory res = new bytes(length); + for (uint256 i = 0; i < length; i++) { + res[i] = data[start + i]; } + return res; } } diff --git a/test/zkbob/ZkBobPoolDecentralized.t.sol b/test/zkbob/ZkBobPoolDecentralized.t.sol new file mode 100644 index 0000000..58c7b9e --- /dev/null +++ b/test/zkbob/ZkBobPoolDecentralized.t.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.15; + +import {AbstractZkBobPoolTestBase} from "./ZkBobPool.t.sol"; +import {AllowListOperatorManager} from "../../src/zkbob/manager/AllowListOperatorManager.sol"; +import {IOperatorManager} from "../../src/interfaces/IOperatorManager.sol"; +import {IBatchDepositVerifier} from "../../src/interfaces/IBatchDepositVerifier.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../shared/ForkTests.t.sol"; + +abstract contract AbstractZkBobPoolDecentralizedTest is AbstractZkBobPoolTestBase { + AllowListOperatorManager manager; + + address proxy1 = makeAddr("Proxy #1"); + address prover1 = makeAddr("Prover #1"); + address feeReceiver1 = makeAddr("Fee Receiver #1"); + + address proxy2 = makeAddr("Proxy #2"); + address prover2 = makeAddr("Prover #2"); + address feeReceiver2 = makeAddr("Fee Receiver #2"); + + address notAllowedProxy = makeAddr("Not Allowed Proxy"); + address notAllowedProver = makeAddr("Not Allowed Prover"); + + address[] operators = [prover1, prover2, proxy1, proxy2]; + address[] feeReceivers = [feeReceiver1, feeReceiver2, feeReceiver1, feeReceiver2]; + + function setUp() public override { + super.setUp(); + + manager = new AllowListOperatorManager(operators, feeReceivers, true); + pool.setOperatorManager(IOperatorManager(manager)); + } + + function testOnlyAllowedProxiesCanTransact() public { + deal(token, user1, 100 ether / D); + + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, prover1); + _transact(data1, proxy1); + + bytes memory data2 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy2, prover2); + _transact(data2, proxy2); + + bytes memory data3 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, notAllowedProxy); + _transactExpectRevert(data3, notAllowedProxy, "ZkBobPool: not an operator"); + + manager.setAllowListEnabled(false); + _transact(data3, notAllowedProxy); + } + + function testOnlyPrivilegedProverCanUpdateTreeWithinGracePeriod() public { + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, prover1); + _transact(data1, proxy1); + + vm.warp(block.timestamp + pool.gracePeriod()); + + _proveTreeUpdateExpectRevert(proxy1, "ZkBobPool: prover is not allowed to submit the proof yet"); + _proveTreeUpdateExpectRevert(prover2, "ZkBobPool: prover is not allowed to submit the proof yet"); + + _proveTreeUpdate(prover1); + } + + function testAnyAllowedProverCanUpdateTreeAfterGracePeriod() public { + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, prover1); + _transact(data1, proxy1); + + vm.warp(block.timestamp + pool.gracePeriod() + 1); + + _proveTreeUpdate(prover2); + } + + function testAnyAllowedProverCanUpdateTreeIfProverIsNotAssigned() public { + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, address(0)); + _transact(data1, proxy1); + _proveTreeUpdate(prover2); + } + + function testNotAllowedProverCantUpdateTreeEvenAfterGracePeriod() public { + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, address(0)); + _transact(data1, proxy1); + + vm.warp(block.timestamp + pool.gracePeriod() + 1); + + _proveTreeUpdateExpectRevert(notAllowedProver, "ZkBobPool: not an operator"); + } + + function testGracePeriodsMayIntersect() public { + deal(token, user1, 100 ether / D); + + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, prover1); + _transact(data1, proxy1); + + bytes memory data2 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy2, prover2); + _transact(data2, proxy2); + + vm.warp(block.timestamp + pool.gracePeriod()); + _proveTreeUpdate(prover1); + + vm.warp(block.timestamp + 1); + _proveTreeUpdate(prover1); + } + + function testFeeDistribution() public { + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.017 ether / D, 0.005 ether / D, proxy1, address(0)); + _transact(data1, proxy1); + assertEq(pool.accumulatedFee(proxy1), 0.017 ether / (D * denominator)); + + vm.prank(feeReceiver1); + pool.withdrawFee(proxy1, feeReceiver1); + + _proveTreeUpdate(prover2); + assertEq(pool.accumulatedFee(prover2), 0.005 ether / (D * denominator)); + + vm.prank(feeReceiver2); + pool.withdrawFee(prover2, feeReceiver2); + + assertEq(pool.accumulatedFee(prover1), 0); + assertEq(pool.accumulatedFee(prover2), 0); + + assertEq(IERC20(token).balanceOf(feeReceiver1), 0.017 ether / D); + assertEq(IERC20(token).balanceOf(feeReceiver2), 0.005 ether / D); + assertEq(IERC20(token).balanceOf(address(pool)), 0.5 ether / D); + assertEq(IERC20(token).balanceOf(user1), 0.478 ether / D); // user1 has 1 ether before the deposit + } + + function testCantSkipCommitments() public { + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, prover1); + _transact(data1, proxy1); + + vm.expectRevert("ZkBobPool: commitment mismatch"); + vm.prank(prover1); + pool.proveTreeUpdate(_randFR(), _randProof(), _randFR()); + } + + function testCantTransactIfTreeUpdateFeeIsLessThenMin() public { + deal(token, user1, 100 ether / D); + pool.setMinTreeUpdateFee(uint64(0.01 ether / (D * denominator))); + + bytes memory data1 = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.009 ether / D, proxy1); + _transactExpectRevert(data1, proxy1, "ZkBobPool: tree update fee is too low"); + + bytes memory data2 = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.01 ether / D, proxy1); + _transact(data2, proxy1); + + bytes memory data3 = _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.011 ether / D, proxy1); + _transact(data3, proxy1); + } + + function testIndexInMessageEventIsConstructedCorrectly() public { + uint256 index = pool.pool_index(); + deal(token, user1, 100 ether / D); + + bytes memory data1 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy1, prover1); + vm.expectEmit(true, false, false, false); + bytes memory message = new bytes(0); + emit Message(index + 128, bytes32(0), message); + _transact(data1, proxy1); + + bytes memory data2 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy2, prover2); + vm.expectEmit(true, false, false, false); + emit Message(index + 2 * 128, bytes32(0), message); + _transact(data2, proxy2); + + (uint256[] memory indices, uint256 commitment, uint256[8] memory proof) = _prepareRandomDirectDeposits(0); + vm.expectEmit(true, false, false, false); + emit Message(index + 3 * 128, bytes32(0), message); + _appendDirectDeposits(indices, commitment, proof, proxy1, prover1); + + _proveTreeUpdate(prover1); + _proveTreeUpdate(prover2); + + bytes memory data3 = + _encodePermitDeposit(int256(0.5 ether / D), 0.005 ether / D, 0.005 ether / D, proxy2, prover2); + vm.expectEmit(true, false, false, false); + emit Message(index + 4 * 128, bytes32(0), message); + _transact(data3, proxy2); + + _proveTreeUpdate(prover1); + _proveTreeUpdate(prover2); + + (indices, commitment, proof) = _prepareRandomDirectDeposits(2); + vm.expectEmit(true, false, false, false); + emit Message(index + 5 * 128, bytes32(0), message); + _appendDirectDeposits(indices, commitment, proof, proxy2, prover2); + + _proveTreeUpdate(prover2); + } + + function testDirectDepositsTreeUpdateFeeTooLow() public { + _setUpDD(); + + pool.setMinTreeUpdateFee(uint64(3)); + queue.setDirectDepositFee(uint64(1)); + + 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; + indices[1] = 1; + + uint256 outCommitment = _randFR(); + vm.prank(proxy1); + vm.expectRevert("ZkBobPool: tree update fee is too low"); + pool.appendDirectDeposits(indices, outCommitment, _randProof(), prover1); + } + + function testDirectDepositsTreeFeesAccrued() public { + _setUpDD(); + + uint64 minTreeUpdateFee = uint64(0.01 ether / (D * denominator)); + uint64 singleDirectDepositFee = uint64(0.1 ether / (D * denominator)); + + pool.setMinTreeUpdateFee(minTreeUpdateFee); + queue.setDirectDepositFee(singleDirectDepositFee); + + 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; + indices[1] = 1; + + uint256 outCommitment = _randFR(); + vm.prank(proxy1); + pool.appendDirectDeposits(indices, outCommitment, _randProof(), prover1); + uint64 expectedFee = uint64(singleDirectDepositFee * 2 - minTreeUpdateFee); + assertEq(expectedFee, pool.accumulatedFee(proxy1)); + + vm.prank(proxy1); + pool.withdrawFee(proxy1, feeReceiver1); + assertEq(IERC20(token).balanceOf(feeReceiver1), expectedFee * denominator); + + _proveTreeUpdate(prover1); + assertEq(minTreeUpdateFee, pool.accumulatedFee(prover1)); + + vm.prank(prover1); + pool.withdrawFee(prover1, feeReceiver1); + assertEq(IERC20(token).balanceOf(feeReceiver1), (expectedFee + minTreeUpdateFee) * denominator); + } + + function _transact(bytes memory _data, address caller) internal { + vm.prank(caller); + (bool status,) = address(pool).call(_data); + assertTrue(status); + } + + function _transactExpectRevert(bytes memory _data, address caller, string memory expectedRevertReason) internal { + vm.prank(caller); + (bool status, bytes memory data) = address(pool).call(_data); + assertFalse(status); + assembly { + data := add(data, 0x04) + } + bytes memory revertReason = abi.decode(data, (bytes)); + assertEq(revertReason, bytes(expectedRevertReason)); + } + + function _proveTreeUpdate(address caller) internal { + (uint256 commitment,,,,) = pool.pendingCommitment(); + vm.prank(caller); + pool.proveTreeUpdate(commitment, _randProof(), _randFR()); + } + + function _proveTreeUpdateExpectRevert(address caller, string memory expectedRevertReason) internal { + (uint256 commitment,,,,) = pool.pendingCommitment(); + vm.expectRevert(bytes(expectedRevertReason)); + vm.prank(caller); + pool.proveTreeUpdate(commitment, _randProof(), _randFR()); + } + + function _prepareRandomDirectDeposits( + uint256 offset + ) + internal + returns (uint256[] memory indices, uint256 commitment, uint256[8] memory proof) + { + _setUpDD(); + + vm.startPrank(user1); + _directDeposit(1 ether / D, user2, zkAddress); + _directDeposit(1 ether / D, user2, zkAddress); + vm.stopPrank(); + + indices = new uint256[](2); + indices[0] = offset + 0; + indices[1] = offset + 1; + + commitment = _randFR(); + proof = _randProof(); + } + + function _appendDirectDeposits( + uint256[] memory indices, + uint256 commitment, + uint256[8] memory proof, + address proxy, + address prover + ) + internal + { + vm.prank(proxy); + pool.appendDirectDeposits(indices, commitment, proof, prover); + } +} + +contract ZkBobPoolBOBPolygonDecentralizedTest is AbstractZkBobPoolDecentralizedTest, 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 ZkBobPoolETHMainnetDecentralizedTest is AbstractZkBobPoolDecentralizedTest, 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 ZkBobPoolDAIMainnetDecentralizedTest is AbstractZkBobPoolDecentralizedTest, 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 ZkBobPoolUSDCPolygonDecentralizedTest is AbstractZkBobPoolDecentralizedTest, 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/manager/AllowListOperatorManager.t.sol b/test/zkbob/manager/AllowListOperatorManager.t.sol new file mode 100644 index 0000000..67e2d24 --- /dev/null +++ b/test/zkbob/manager/AllowListOperatorManager.t.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.15; + +import {Test} from "forge-std/Test.sol"; +import {AllowListOperatorManager} from "../../../src/zkbob/manager/AllowListOperatorManager.sol"; +import "../../shared/Env.t.sol"; + +contract AllowListOperatorManagerTest is Test { + address operator1 = makeAddr("operator1"); + address feeReceiver1 = makeAddr("feeReceiver1"); + + address operator2 = makeAddr("operator2"); + address feeReceiver2 = makeAddr("feeReceiver2"); + + address unauthorized = makeAddr("unauthorized"); + address unauthorizedFeeReceiver = makeAddr("unauthorizedFeeReceiver"); + + AllowListOperatorManager manager; + + function testConstructor() public { + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + address[] memory feeReceivers = new address[](1); + feeReceivers[0] = feeReceiver1; + + vm.expectRevert("OperatorManager: arrays length mismatch"); + manager = new AllowListOperatorManager(operators, feeReceivers, true); + + feeReceivers = new address[](2); + feeReceivers[0] = feeReceiver1; + feeReceivers[1] = feeReceiver2; + operators[0] = address(0); + + vm.expectRevert("OperatorManager: zero address"); + manager = new AllowListOperatorManager(operators, feeReceivers, true); + + operators[0] = operator1; + manager = new AllowListOperatorManager(operators, feeReceivers, true); + assertEq(manager.owner(), address(this)); + assertTrue(manager.allowListEnabled()); + + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + + assertTrue(manager.isOperator(operator2)); + assertTrue(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + + assertFalse(manager.isOperator(unauthorized)); + assertFalse(manager.isOperator(address(this))); + + manager = new AllowListOperatorManager(operators, feeReceivers, false); + assertEq(manager.owner(), address(this)); + assertFalse(manager.allowListEnabled()); + + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + assertTrue(manager.isOperator(operator2)); + assertTrue(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + + assertTrue(manager.isOperator(unauthorized)); + assertTrue(manager.isOperator(makeAddr("random"))); + } + + function testOperatorURI() public { + manager = new AllowListOperatorManager(new address[](0), new address[](0), true); + assertEq(manager.operatorURI(), ""); + } + + function testSetAllowListEnabled() public { + address[] memory operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + address[] memory feeReceivers = new address[](2); + feeReceivers[0] = feeReceiver1; + feeReceivers[1] = feeReceiver2; + + manager = new AllowListOperatorManager(operators, feeReceivers, true); + assertTrue(manager.allowListEnabled()); + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + assertTrue(manager.isOperator(operator2)); + assertTrue(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + assertFalse(manager.isOperator(unauthorized)); + + vm.prank(makeAddr("not owner")); + vm.expectRevert("Ownable: caller is not the owner"); + manager.setAllowListEnabled(false); + + manager.setAllowListEnabled(false); + assertFalse(manager.allowListEnabled()); + assertTrue(manager.isOperator(unauthorized)); + + manager.setAllowListEnabled(true); + assertTrue(manager.allowListEnabled()); + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + assertTrue(manager.isOperator(operator2)); + assertTrue(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + assertFalse(manager.isOperator(unauthorized)); + } + + function testSetOperator() public { + address[] memory operators = new address[](1); + operators[0] = operator1; + + address[] memory feeReceivers = new address[](1); + feeReceivers[0] = feeReceiver1; + + manager = new AllowListOperatorManager(operators, feeReceivers, true); + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + assertFalse(manager.isOperator(operator2)); + assertFalse(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + + vm.prank(makeAddr("not owner")); + vm.expectRevert("Ownable: caller is not the owner"); + manager.setOperator(operator2, feeReceiver2, true); + + manager.setOperator(operator2, feeReceiver2, true); + assertTrue(manager.isOperator(operator2)); + assertTrue(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + + manager.setOperator(operator1, feeReceiver1, false); + assertFalse(manager.isOperator(operator1)); + // Even if operator was removed, we still allow to claim accumulated fees + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + + manager.setOperator(operator1, feeReceiver1, true); + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + assertTrue(manager.isOperator(operator2)); + assertTrue(manager.isOperatorFeeReceiver(operator2, feeReceiver2)); + assertFalse(manager.isOperator(unauthorized)); + } + + function testSetOperators() public { + address[] memory operators = new address[](1); + operators[0] = operator1; + + address[] memory feeReceivers = new address[](1); + feeReceivers[0] = feeReceiver1; + + manager = new AllowListOperatorManager(operators, feeReceivers, true); + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + + operators = new address[](2); + operators[0] = operator1; + operators[1] = operator2; + + feeReceivers = new address[](1); + feeReceivers[0] = feeReceiver1; + + bool[] memory allowed = new bool[](2); + allowed[0] = true; + allowed[1] = true; + + vm.expectRevert("OperatorManager: arrays length mismatch"); + manager.setOperators(operators, allowed, feeReceivers); + + feeReceivers = new address[](2); + feeReceivers[0] = feeReceiver1; + feeReceivers[1] = feeReceiver2; + + allowed = new bool[](1); + allowed[0] = true; + + vm.expectRevert("OperatorManager: arrays length mismatch"); + manager.setOperators(operators, allowed, feeReceivers); + + operators = new address[](10); + operators[0] = operator1; + feeReceivers = new address[](10); + feeReceivers[0] = address(0); + allowed = new bool[](10); + for (uint256 i = 1; i < 10; i++) { + operators[i] = address(uint160(i)); + feeReceivers[i] = address(uint160(2 * i + 1)); + allowed[i] = i % 2 != 0; + } + + vm.prank(makeAddr("not owner")); + vm.expectRevert("Ownable: caller is not the owner"); + manager.setOperators(operators, allowed, feeReceivers); + + manager.setOperators(operators, allowed, feeReceivers); + for (uint256 i = 0; i < 10; i++) { + assertEq(manager.isOperator(operators[i]), allowed[i]); + if (i > 0) { + assertEq(manager.isOperatorFeeReceiver(operators[i], feeReceivers[i]), allowed[i]); + } + } + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + } + + function testSetFeeReceiver() public { + address[] memory operators = new address[](1); + operators[0] = operator1; + + address[] memory feeReceivers = new address[](1); + feeReceivers[0] = feeReceiver1; + + manager = new AllowListOperatorManager(operators, feeReceivers, true); + assertTrue(manager.isOperator(operator1)); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver1)); + + vm.prank(operator1); + manager.setFeeReceiver(feeReceiver2); + assertTrue(manager.isOperatorFeeReceiver(operator1, feeReceiver2)); + + vm.prank(unauthorized); + vm.expectRevert("OperatorManager: operator not allowed"); + manager.setFeeReceiver(unauthorizedFeeReceiver); + } +}