Forced Exit

Emergency funds withdrawal in case the relayer is unavailable

The zkBob solution involves interacting with the pool through a relayer. You cannot send deposit, transfer, or withdrawal transactions directly to the contract. This is done to facilitate interaction with the pool without the need to pay fees in the native coin. If the relayer subsystem is unavailable for the any reason, there is an option for emergency withdrawal from the pool. In this case, you must interact directly with the pool contract.

In the event of an emergency exit, all available funds will be withdrawn to the specified address, but the zk-account will be destroyed, and you will no longer be able to use it for transactions.

An emergency exit is performed in two stages. In the first stage, you request an exit window from the pool contract, specifying all the necessary parameters in advance, such as the withdrawal address, the amount of funds, and the computed proof for the withdrawal transaction. This operation is known as commit. The exit window opens 1 hour after the commitment and is available for 24 hours after the commit.

The second stage can be completed only during the window's validity period. To perform it you must send an emergency exit execution transaction.

If you miss the exit window, you will need to cancel the previously committed emergency exit in order to request a new exit window.

If the relayer's functionality is restored before executing an emergency exit, and you send any regular transaction to the pool, the existing emergency exit will be invalidated.

If the relayer is in the operational state it will block your transactions from your account during the active exit window.

After a successful emergency exit, your account is marked as destroyed on the pool contract and all incoming transfers to that account will burned.

It's important to note that in the case of an emergency exit execution, only the exact amount specified in the commit transaction will be withdrawn. If you receive an incoming transfer after the commit but before the withdrawal is executed, it will not be withdrawn. In this case, you must wait until the exit window expires, cancel the exit, and then create a new one (which takes into account the received funds).

The following routines guide you through the emergency exit procedure and help to retrieve related info.

Checking If Account Destroyed

The account can be destroyed only during execution of the forced exit.

async isAccountDead(): Promise<boolean>

This is a single function related to the emergency exit flow which works even if the forced exit feature is unsupported by the privacy pool. For other routines you should check forced exit support first.

Returns

Promise returns boolean indicating if the forced exit procedure was performed for the account.

Example

if (await zkClient.isAccountDead()) {
    console.log('Sorry, you cannot transact anymore: your account was destroyed');
} else {
    console.log('Forced exit was not performed yet');
}
// output: Forced exit was not performed yet

Checking If Emergency Exit is Supported by Current Pool

The forced exit functionality can be unsupported in the certain pools. Use the following routine to check if this feature is supported.

async isForcedExitSupported(): Promise<boolean>

Returns

Promise returns boolean indicating if the forced exit is supported by the current pool contract.

Example

if (await zkClient.isForcedExitSupported()) {
    console.log('Okay, let\'s try to withdraw your funds emergency');
    // ...forced-exit related flow...
} else {
    console.log('Sorry, emergency exit is unsupported by the pool');
}
// output: Okay, let's try to withdraw your funds emergency

Getting Forced Exit State for Account

To check forced exit state for your current account you can use the following routine:

async forcedExitState(): Promise<ForcedExitState>

Returns

Promise returns ForcedExitState enum member.

Example

switch (await zkClient.forcedExitState()) {
    case ForcedExitState.NotStarted:
        console.log('Forced exit was not started');
        break;
    case ForcedExitState.CommittedWaitingSlot:
        console.log('Forced exit commited but not available yet');
        break;
    case ForcedExitState.CommittedReady:
        console.log('Forced exit commited and ready to execute');
        break;
    case ForcedExitState.Completed:
        console.log('Forced exit completed');
        break;
    case ForcedExitState.Outdated:
        console.log('Forced exit outdated');
        break;
    default:
        console.error('Unknown forced exit state');
        break;
}
// output: Forced exit was not started

Getting Committed Forced Exit Details

When you commit a forced exit the set of parameters should be specified. The following method retrieves the committed forced exit parameters (if the exist).

async activeForcedExit(): Promise<CommittedForcedExit | undefined>

This method returns a forced exit object only in the following states: CommittedWaitingSlot, CommittedReady, Outdated

The result is undefined in case the forced exit was executed, cancelled, or not started

Returns

Promise returns CommittedForcedExit object or undefined if unavailable.

Example

const committed = await zkClient.activeForcedExit()
if (committed) {
    console.log('Found committed forced exit');
    console.log(`Nullifier:  ${committed.nullifier}`);
    console.log(`Operator:   ${committed.operator}`);
    console.log(`Receiver:   ${committed.to}`);
    console.log(`Amount:     ${committed.amount}`);
    console.log(`Start time: ${new Date(committed.exitStart * 1000).toLocaleString()}`);
    console.log(`End time:   ${new Date(committed.exitEnd * 1000).toLocaleString()}`);
    console.log(`Tx hash:    ${committed.txHash`);
}
// Found committed forced exit
// Nullifier:  7151204415055949250553114361348271896976012357469097285489034137873972583466
// Operator:   0x6D16337B9a1651556749230eeAA4Dc602A22DcAf
// Receiver:   0x6D16337B9a1651556749230eeAA4Dc602A22DcAf
// Amount:     677000000000
// Start time: 24.10.2023, 21:49:12
// End time:   25.10.2023, 20:49:12
// Tx hash:    0x2cb5eb49f7b89810675fce988f259b7f47a63710029a303ab0906b59a5bba7b2

Getting Completed Forced Exit Details

The following structure describes the second stage result. It's available only in the ForcedExitState.Completed state.

async executedForcedExit(): Promise<FinalizedForcedExit | undefined>

Although FinalizedForcedExit object contains the cancelled property which can indicate the forced exit was cancelled, the current method doesn't return an object for the cancelled forced exit (because it's difficult to locate it due to nullifier updates).

Returns

Promise returns FinalizedForcedExit object or undefined if unavailable.

Example

const executed = await zkClient.executedForcedExit()
if (executed) {
    console.log('Found completed forced exit');
    console.log(`Nullifier:  ${executed.nullifier}`);
    console.log(`Receiver:   ${executed.to}`);
    console.log(`Amount:     ${executed.amount}`);
    console.log(`Tx hash:    ${executed.txHash`);
}
// Found completed forced exit
// Nullifier:  13575656066236883124312534101991453259859429227583973074085956794292119880492
// Receiver:   0x6e9aE59020788AfCaAb243FCD0e9317189a81435
// Amount:     7000000000
// Tx hash:    0x6df714f0e6100f1755611e0f3e58e23111b4642a3f437fca0062b685ab84db9e

Checking Funds Available for Forced Exit

You should always check available funds and compare with the account balance before committing to the forced exit.

Keep in mind your account configuration may consist of a lot of incoming transfers (notes) which cannot be withdrawn within a single transaction. For the forced exit flow you can use just an account balance with three notes. You cannot initiate an aggregation transaction to collect unspent notes by direct pool contract interaction. Additional details about notes handling can be found here

The forced exit flow doesn't require the relayer so you shouldn't pay the relayer fee. But you should have a few native coins to pay the pool contract transaction fee.

async availableFundsToForcedExit(): Promise<bigint>

Returns

Promise returns amount of tokens in pool dimension which can be withdrawn during the forced exit.

Example

const feFunds = await zkClient.availableFundsToForcedExit();
const balance = await zkClient.getTotalBalance();
if (feFunds < balance) {
    console.log(`You can withdraw just ${feFunds} of ${balance}`);
} else {
    console.log(`You can withdraw all of your balance (${feFunds})`);
}
// output: You can withdraw all of your balance (677000000000)

Making Commit Forced Exit

This is the first stage of the emergency exit flow. The following method will prepare all values needed to send the commit transaction and invoke a callback to send it.

async requestForcedExit(
    executerAddress: string,
    toAddress: string,
    sendTxCallback: (tx: PreparedTransaction) => Promise<string>
  ): Promise<CommittedForcedExit>

Parameters

executerAddress - who will be able to send execute transaction (set to zero address to disable constraints).

toAddress - who will receive funds during forced exit execution.

sendTxCallback - callback will be invoked by this method when commit transaction should be sent. You should sign and send the requested transaction (described by PreparedTransaction) in your callback and return a transaction hash.

Returns

Promise returns CommittedForcedExit object or undefined if unavailable

Example

const committed = await zkClient.requestForcedExit(
    '0x6D16337B9a1651556749230eeAA4Dc602A22DcAf',
    '0x6D16337B9a1651556749230eeAA4Dc602A22DcAf',
    async (tx: PreparedTransaction) => 
        this.sendAndSendTx(tx.to, tx.amount, tx.data);
);
    
if (committed) {
    console.log(`Forced exit has been committed ${committed.txHash}`);
    console.log(`Wait until ${new Date(committed.exitStart * 1000).toLocaleString()}`);
}
// Forced exit has been committed 0x2cb5eb49f7b89810675fce988f259b7f47a63710029a303ab0906b59a5bba7b2
// Wait until 24.10.2023, 21:49:12

Executing Forced Exit

Use the following routine to complete the committed forced exit. It will automatically collect all needed data and invoke the callback to send a transaction.

This routine should be invoked only in ForcedExitState.CommittedReady state otherwise an InternalError will thrown

async executeForcedExit(
    sendTxCallback: (tx: PreparedTransaction) => Promise<string>
): Promise<FinalizedForcedExit>

Parameters

sendTxCallback - callback will be invoked by this method when execute transaction should be sent. You should sign and send requested transaction (described by PreparedTransaction) in your callback and return a transaction hash

Returns

Promise returns FinalizedForcedExit object or undefined if unavailable

Example

const completed = await zkClient.executeForcedExit(
    async (tx: PreparedTransaction) => 
        this.sendAndSendTx(tx.to, tx.amount, tx.data);
);
    
if (completed) {
    console.log(`Forced exit has been completed: ${completed.txHash}`);
    console.log(`Check address ${completed.to} for withdrawn funds`);
}
// Forced exit has been completed: 0x6df714f0e6100f1755611e0f3e58e23111b4642a3f437fca0062b685ab84db9e
// Check address 0x6e9aE59020788AfCaAb243FCD0e9317189a81435 for withdrawn funds

Cancelling Forced Exit

If you miss the exit window, you need to first cancel the exit (with the current nullifier) before creating a new one. Use the following method:

This routine should be invoked only in ForcedExitState.Outdated state otherwise an InternalError will thrown

async cancelForcedExit(
    sendTxCallback: (tx: PreparedTransaction) => Promise<string>
): Promise<FinalizedForcedExit>

Parameters

sendTxCallback - callback will be invoked by this method when cancel transaction should be sent. You should sign and send requested transaction (described by PreparedTransaction) in your callback and return a transaction hash.

Returns

Promise returns FinalizedForcedExit object or undefined if unavailable.

Example

const cancelled = await zkClient.cancelForcedExit(
    async (tx: PreparedTransaction) => this.sendAndSendTx(tx.to, tx.amount, tx.data);
);
    
if (cancelled) {
    console.log(`Forced exit has been cancelled: ${cancelled.txHash}`);
}
// Forced exit has been cancelled: 0x81039f753be2078a467370947807fdbdca571630f21920b6d6337b305fb488d4

Last updated