Skip to content

Commit

Permalink
Add forced exit (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
k1rill-fedoseev authored Sep 11, 2023
1 parent 08b974b commit 4461aa1
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 3 deletions.
1 change: 0 additions & 1 deletion script/scripts/DeployZkBobPoolModules.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "./Env.s.sol";
import "../../src/zkbob/ZkBobPool.sol";
import "../../src/zkbob/utils/ZkBobAccounting.sol";
import "../../src/proxy/EIP1967Proxy.sol";
import "../../src/zkbob/ZkBobPoolBOB.sol";
import "../../src/zkbob/ZkBobPoolUSDC.sol";

contract DummyDelegateCall {
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IZkBobAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ interface IZkBobAccounting {
enum TxType {
Common,
DirectDeposit,
AppendDirectDeposits
AppendDirectDeposits,
ForcedExit
}

struct Limits {
Expand Down
131 changes: 130 additions & 1 deletion src/zkbob/ZkBobPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex

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;

uint256 internal immutable TOKEN_DENOMINATOR;
uint256 internal constant TOKEN_NUMERATOR = 1;
Expand All @@ -45,7 +47,8 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex
address public immutable token;
IZkBobDirectDepositQueue public immutable direct_deposit_queue;

uint256[3] private __deprecatedGap;
uint256[2] private __deprecatedGap;
mapping(uint256 => bytes32) public committedForcedExits;
IEnergyRedeemer public redeemer;
IZkBobAccounting public accounting;
uint96 public pool_index;
Expand All @@ -65,6 +68,11 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex

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 ForcedExit(uint256 indexed index, uint256 indexed nullifier, address to, uint256 amount);

constructor(
uint256 __pool_id,
address _token,
Expand Down Expand Up @@ -330,6 +338,104 @@ abstract contract ZkBobPool is IZkBobPool, EIP1967Admin, Ownable, Parameters, Ex
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 <= 1 << 63, "ZkBobPool: amount too large");
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");

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
);
}

/**
* @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.
*/
function executeForcedExit(
uint256 _nullifier,
address _operator,
address _to,
uint256 _amount,
uint256 _exitStart,
uint256 _exitEnd
)
external
{
require(nullifiers[_nullifier] == 0, "ZkBobPool: doublespend detected");
require(
committedForcedExits[_nullifier] == _hashForcedExit(_operator, _to, _amount, _exitStart, _exitEnd),
"ZkBobPool: invalid forced exit"
);

require(_operator == address(0) || _operator == msg.sender, "ZkBobPool: invalid caller");
require(block.timestamp >= _exitStart && block.timestamp < _exitEnd, "ZkBobPool: exit not allowed");

(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);

IERC20(token).safeTransfer(_to, _amount * TOKEN_DENOMINATOR / TOKEN_NUMERATOR);

emit ForcedExit(poolIndex, _nullifier, _to, _amount);
}

/**
* @dev Records submitted direct deposit into the users limits.
* Callable only by the direct deposit queue.
Expand Down Expand Up @@ -362,6 +468,29 @@ 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.
Expand Down
5 changes: 5 additions & 0 deletions src/zkbob/utils/ZkBobAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ 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;
Expand Down
21 changes: 21 additions & 0 deletions test/interfaces/IZkBobPoolAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ interface IZkBobPoolAdmin {

function transact() external;

function commitForcedExit(
address _operator,
address _to,
uint256 _amount,
uint256 _index,
uint256 _nullifier,
uint256 _out_commit,
uint256[8] memory _transfer_proof
)
external;

function executeForcedExit(
uint256 _nullifier,
address _operator,
address _to,
uint256 _amount,
uint256 _exitStart,
uint256 _exitEnd
)
external;

function appendDirectDeposits(
uint256 _root_after,
uint256[] calldata _indices,
Expand Down
44 changes: 44 additions & 0 deletions test/zkbob/ZkBobPool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,50 @@ 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);

vm.expectRevert("ZkBobPool: invalid forced exit");
pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, block.timestamp, exitEnd);

vm.expectRevert("ZkBobPool: invalid caller");
pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd);

vm.startPrank(user2);
vm.expectRevert("ZkBobPool: exit not allowed");
pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd);

skip(25 hours);
vm.expectRevert("ZkBobPool: exit not allowed");
pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd);

rewind(23 hours);
pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd);

vm.expectRevert("ZkBobPool: doublespend detected");
pool.executeForcedExit(nullifier, user2, user2, 0.4 ether / D / denominator, exitStart, exitEnd);

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 testRejectNegativeDeposits() public {
bytes memory data1 = _encodePermitDeposit(int256(0.99 ether / D), 0.01 ether / D);
_transact(data1);
Expand Down

0 comments on commit 4461aa1

Please sign in to comment.