diff --git a/src/zkbob/ZkBobPool.sol b/src/zkbob/ZkBobPool.sol index 3e933a1..c0512cc 100644 --- a/src/zkbob/ZkBobPool.sol +++ b/src/zkbob/ZkBobPool.sol @@ -71,6 +71,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex 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); constructor( @@ -369,6 +370,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex 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, @@ -405,6 +407,7 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex * @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. */ function executeForcedExit( uint256 _nullifier, @@ -412,7 +415,8 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex address _to, uint256 _amount, uint256 _exitStart, - uint256 _exitEnd + uint256 _exitEnd, + bool _cancel ) external { @@ -421,6 +425,13 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex 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; + } require(_operator == address(0) || _operator == msg.sender, "ZkBobPool: invalid caller"); require(block.timestamp >= _exitStart && block.timestamp < _exitEnd, "ZkBobPool: exit not allowed"); diff --git a/test/interfaces/IZkBobPoolAdmin.sol b/test/interfaces/IZkBobPoolAdmin.sol index cd37b45..b215451 100644 --- a/test/interfaces/IZkBobPoolAdmin.sol +++ b/test/interfaces/IZkBobPoolAdmin.sol @@ -27,6 +27,8 @@ interface IZkBobPoolAdmin { function transact() external; + function committedForcedExits(uint256 _nullifier) external view returns (bytes32); + function commitForcedExit( address _operator, address _to, @@ -44,7 +46,8 @@ interface IZkBobPoolAdmin { address _to, uint256 _amount, uint256 _exitStart, - uint256 _exitEnd + uint256 _exitEnd, + bool _cancel ) external; diff --git a/test/zkbob/ZkBobPool.t.sol b/test/zkbob/ZkBobPool.t.sol index 8316b90..8428f9b 100644 --- a/test/zkbob/ZkBobPool.t.sol +++ b/test/zkbob/ZkBobPool.t.sol @@ -317,27 +317,27 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { assertEq(pool.nullifiers(nullifier), 0); vm.expectRevert("ZkBobPool: invalid forced exit"); - pool.executeForcedExit(nullifier ^ 1, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd); + 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); + 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); + 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); + 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); + 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); + 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); + 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); @@ -348,6 +348,39 @@ abstract contract AbstractZkBobPoolTest is AbstractForkTest { 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); _transact(data1);