Withdrawals
Withdrawals work quite differently from deposits and follow a very complex architecture on the L1 and L2 to make sure they work in all possible scenarios.
They generally commence in three stages:
- User-sent L2 withdrawal transaction is sequenced and executed
- Withdrawal is part of the next settlement
- Withdrawals get unrolled onto the L1 iteratively.
- Withdrawals get claimed by the user
Steps 3. and 4. might be a bit confusing, so let’s walk through that mechanism in detail:
Because of Mina’s account update limit, the process to get all L2 withdrawal onto the L1’s ledger is by iteratively unrolling them. This happens in batches of 6 and can be executed by anyone (doesn’t have to be the sequencer, but will be in most cases).
Now, because of the way that the L1’s zkapps protocol is designed, we have to use an extra step before the tokens are actually in the user’s wallet. During this step, every users gets their withdrawal minted as a special custom token on the L1. This custom token is managed by the protokit bridging contract and does two things: It mints customs tokens during the unrolling, and it lets users redeem those custom tokens for the real tokens. This claiming step is done by each user individually at their own pace. Further reasoning and design choices can be found in this page’s references
Implementing withdrawals
The protokit library already offers a pre-built Withdrawals runtime module, that provides a implementation for
token withdrawals.
On the other side, the default BridgingContract offers processing of token withdrawals on the L1 side.
Withdrawing tokens
Once the Withdrawals runtime module is wired up and the BridgeContract is deployed on the L1,
users can start withdrawing their tokens back from the L2 to the L1.
A withdrawal is always a two-step flow for the end user:
- Withdraw on the L2 — the user sends an L2 transaction to the
Withdrawalsruntime module, which burns the balance on the appchain and enqueues a withdrawal message. After the next settlement and the subsequent unrolling batches, the withdrawal arrives on the L1 as a balance of the bridge’s custom token held by the recipient. - Redeem on the L1 — once the withdrawal has been unrolled onto the L1, the user sends an L1 transaction to the
BridgeContractthat burns the custom token and releases the real tokens (MINA or a fungible token) into their wallet.
You can trigger both steps either from the protokit CLI or programmatically by calling the BridgingModule directly.
The protokit CLI exposes two bridge commands for the two stages of a withdrawal.
They use the same environment variables your appchain already relies on (dispatcher/bridge address, Mina network, etc.),
so in most cases you only have to provide the amount, token and the signing key.
Step 1 — Withdraw on the L2
This submits the L2 transaction to the Withdrawals runtime module. The tokens are burned on the appchain and
will, after the next settlement and unrolling, be minted to the recipient as the bridge’s custom token on the L1.
# Initiate token withdrawal from the L2 back to the L1
pnpm protokit bridge withdraw <tokenId> <fromPrivateKey> <amount>tokenIdis0for MINA and the custom token’s tokenId for everything else.fromPrivateKeyis the L2 account holding the balance (or the name of an env variable containing it).amountis specified in the token’s base unit
Step 2 — Redeem on the L1
Once the withdrawal has been unrolled onto the L1, the user redeems it to receive the real tokens:
# Redeem the tokens on the L1
pnpm protokit bridge redeem <tokenId> <toKey> <amount>toKeyis the recipient’s public key on the L1 (or the name of an env variable containing it).
Step 2 can only succeed after the withdrawal has actually landed on the L1. That requires the next settlement plus enough unrolling batches to cover your withdrawal - this normally takes a few L1 blocks to finish.