Understanding the Withdrawal Process on Reddio: Moving Assets from L2 to L1

Understanding the Withdrawal Process on Reddio: Moving Assets from L2 to L1

As a Reddio user, after completing various transactions on Layer 2 (L2), you will eventually need to withdraw your assets (NFTs, ETH, etc.) to L1. This blog explains the withdrawal process and how Reddio handles your withdrawal requests.

Reddio is an L2 solution on Ethereum for apps/games and aims to assist developers in handling certain issues relating to scalability and transaction fees so as to enhance the Ethereum network for application and game development.

Preparation: Holding Assets on L2

Firstly, to make a withdrawal, you need to hold assets on L2. If you don't already have assets, you can register on our Dashboard and mint some assets (see demos).

🆓 🆓🆓 Reddio now offers you 1 free goerli for trying Reddio‘s contract deployer. Contact us via our Discord!

Note that these assets will only be visible on L2 and are invisible on L1.

The StarkEx Withdrawal Process

Here's a simple rundown of the withdrawal process on the Reddio dashboard using an NFT as an example:

  1. The user sends a withdrawal request for an NFT to a specific L1 address to Reddio's API.
  2. Reddio verifies the user's ownership of the NFT. Once confirmed, Reddio sends the transaction to StarkEx.
  3. StarkEx completes the verification, packages it, and sends it to the smart contract on L1. The smart contract then moves the withdrawal asset to a location known as the Withdraw Area.
  4. The user interacts directly with the smart contract to withdraw their NFT to any L1 address.

Let’s break down each step and dive deep into each step:

The Reddio Withdrawal Process

The user selects an NFT for withdrawal on the Demos page.

On clicking 'Withdraw,' the browser sends a request to Reddio's API:

  "stark_key": "0x179be264ab70bdc0949bbf3ae2ae3dcefd74562f24bd33459754df1c321f93",
  "amount": "1",
  "receiver": "0xA17B642A47cA576c42339fD3E6a6F7d98478B182",
  "contract_address": "0x1C888Ce67E3BBc82Ebf7a231C6AB25Be97fEc023",
  "token_id": "4",
  "vault_id": "23422102",
  "receiver_vault_id": "23423251",
  "asset_id": "0x400c60fdb5e43da5c2bc5f934ea6887bf2a760bcb2d23dfe6d6e8ef605adcc7",
  "expiration_timestamp": 4194303,
  "signature": {
    "r": "0x6cb78197cb77c0a0612dbb220a6151a32fab11e246e77056691f8bac1492dcc",
    "s": "0x2e54f4972364b7a59f032172461bafaf3c9f990273f19b0b4389f35f03a9dad"
  "nonce": 57

The request includes the following user's details:

  • stark_key (L2 address)
  • the receiver (L1 address for withdrawal)
  • token_id of the asset to be withdrawn
  • asset_id of the asset to be withdrawn

The response will contain a sequence ID provided by Reddio. For example:

  "status": "OK",
  "error": "",
  "error_code": 0,
  "data": {
    "sequence_id": 614051

From the API documentation (https://api-docs.reddio.com/), we can find out that we can pass this sequence_id to the record interface to query the status. The URL is: https://api-dev.reddio.com/v1/record?sequence_id=614051

  "status": "OK",
  "error": "",
  "error_code": 0,
  "data": [
      "amount": "1",
      "asset_id": "0x400c60fdb5e43da5c2bc5f934ea6887bf2a760bcb2d23dfe6d6e8ef605adcc7",
      "asset_name": "ECM",
      "asset_type": "ERC721M",
      "contract_address": "0x1C888Ce67E3BBc82Ebf7a231C6AB25Be97fEc023",
      "display_value": "1",
      "record_type": 4,
      "sequence_id": 614051,
      "stark_key": "0x179be264ab70bdc0949bbf3ae2ae3dcefd74562f24bd33459754df1c321f93",
      "status": 1,
      "time": 1687927839,
      "token_id": "4"

Here, status 1 indicates that this transaction has been accepted by Reddio and has been submitted to StarkEx for packing. The complete status table for Reddio is as follows:

Status Value
SubmittedToReddio 0
AcceptedByReddio 1
FailedOnReddio 2
AcceptedOnL2 3
RejectedOnL2 4
Rolled 5
AcceptedOnL1 6

According to the StarkEx documentation (https://docs.starkware.co/starkex/starkex_playground_tutorial.html), we can see that the packing time here can be up to 18 hours, but in our experience, it usually takes around 4 hours to complete the packing. At that time, the status will also change from 1 (AcceptedByReddio) to 3 (AcceptedOnL2).

To understand the entire lifecycle of this transaction, we can compare StarkEx's Transaction lifecycle with that of Reddio’s, where we know that StarkEx has the following states:

  • Transaction is not yet known to the sequencer.
  • RECEIVED: Transaction was received by the sequencer. Transaction will now either execute successfully or be rejected.
  • PENDING: Transaction executed successfully and entered the pending block.
  • ACCEPTED_ON_L2: Transaction passed validation and entered an actual created block on L2.
  • ACCEPTED_ON_L1: Transaction was accepted on-chain.
  • REJECTED: Transaction executed unsuccessfully and thus was skipped (applies both to a pending and an actual created block). Possible reasons for transaction rejection:

You can see that the Reddio status table aligns almost perfectly with the StarkEx states.

In the steps mentioned above, after Reddio receives the user's Withdraw request, it converts it into a Transfer request and creates a Sequence. Then we construct a MultiTransactionRequest and send it to StarkEx's API interface. According to the StarkEx documentation (https://docs.starkware.co/starkex/spot/withdrawal.html), the transaction sent here needs to be a TransferRequest followed by a WithdrawalRequest.

Specifically, it is sent to StarkEx's Gateway at the /add_transaction endpoint, with content roughly as follows:

  "type": "MultiTransactionRequest",
  "txs": [
      "type": "TransferRequest",
      "amount": "1",
      "token": "0x400c60fdb5e43da5c2bc5f934ea6887bf2a760bcb2d23dfe6d6e8ef605adcc7",
      "type": "WithdrawalRequest",
      "vault_id": "23423251",
      "stark_key": "0xa17b642a47ca576c42339fd3e6a6f7d98478b182",
      "token_id": "0x400c60fdb5e43da5c2bc5f934ea6887bf2a760bcb2d23dfe6d6e8ef605adcc7",
      "amount": "1"

Once this transaction is sent, it will be in the RECEIVED state on StarkEx and will begin transitioning to PENDING and ACCEPTED_ON_L2 states. At this point, the L2 part has already been completed.

The L1 Withdrawal Process

After receiving our request, StarkEx validates the transaction, packages it into a batch, and updates its online status. Our smart contract then emits an event called LogMintableWithdrawalAllowed.

This event signals that the withdrawal asset has reached the Withdraw Area. Alternatively, we can interact with the contract to call the GetWithdrawalBalance method to check if a particular AssetID has reached the Withdraw Area.

Here's an example code snippet in Golang for checking the withdrawal balance:

package main

import (


func main() {
        client, _ := ethclient.Dial("<https://goerli.infura.io/v3/><your_api_key>")
        tokenAddress := common.HexToAddress("0x8Eb82154f314EC687957CE1e9c1A5Dc3A3234DF9")

        OwnerKeyString := "0xA17B642A47cA576c42339fD3E6a6F7d98478B182"
        OwnerKeyBigInt := hexutil.MustDecodeBig(OwnerKeyString)

        // Token ID 4
        AssetIDString := "0x400c60fdb5e43da5c2bc5f934ea6887bf2a760bcb2d23dfe6d6e8ef605adcc7"
        AssetIDBigInt := hexutil.MustDecodeBig(AssetIDString)

        instance, err := withdrawals.NewWithdrawals(tokenAddress, client)
        if err != nil {

        withdrawalBalance, err := instance.GetWithdrawalBalance(&bind.CallOpts{}, OwnerKeyBigInt, AssetIDBigInt)
        if err != nil {


When the value of withdrawalBalance is 1, it indicates that our withdrawal asset has reached the Withdraw Area. We can also observe this through an event. The corresponding event can be found at https://goerli.etherscan.io/tx/0xb7483cd072c1c6cf701b9dbdd2ba64838ad032844e110e76bf900b027e89ba91, and the event name is `LogMintableWithdrawalAllowed`. We can parse the content using the following Golang code:

acceptModificationsContractMeta := GetContractMeta(AcceptModifications)
log := new(types.Log)
err := log.UnmarshalJSON([]byte(Data))
if err != nil {
    fmt.Println("Error parsing ParseLogMintableWithdrawalAllowed raw JSON from event_logs", err)
mintableWithdrawalAllowed := new(acceptModifications.AcceptModificationsLogMintableWithdrawalAllowed)
err = acceptModificationsContractMeta.ToBoundContract().UnpackLog(mintableWithdrawalAllowed, LogMintableWithdrawalAllowed, *log)
if err != nil {
    fmt.Println("Error parsing ParseLogMintableWithdrawalAllowed", err)
OwnerKey := common.BytesToAddress(mintableWithdrawalAllowed.OwnerKey.Bytes()).String()

AssetId := hexutil.Encode(mintableWithdrawalAllowed.AssetId.Bytes())

QuantizedAmount := mintableWithdrawalAllowed.QuantizedAmount.Int64()

By parsing this code, we can obtain the values OwnerKey, AssetId, and QuantizedAmount.

At the same time, in the Withdraw section of the Demo, we can also see that this Token is now available for withdrawal.

At this point, we can click on "Withdraw" to transfer this Token to L1, remember that it originally existed only on L2, but now we're able to transfer it to L1. The withdrawal transaction can be found at https://goerli.etherscan.io/tx/0x08c320aa0f3d76d3a9dd6e9bb53f3460f543f3bce034ae0c72b474edfaa9b3bc.

By observing the contract event (https://goerli.etherscan.io/tx/0x08c320aa0f3d76d3a9dd6e9bb53f3460f543f3bce034ae0c72b474edfaa9b3bc), we can see that the Withdraw and Mint methods were used in the contract. The Mint action is performed only when a user withdraws from L2 to L1 for the first time.

After the withdrawal is completed, we can see that our contract emits a new event called LogMintWithdrawalPerformed, indicating that the corresponding Token has been successfully withdrawn.