This post will present the solution that StarkEx offers for supporting Fast Withdrawals: the ability to withdraw funds from L2 to any L1 address, in blockchain time. This solution is independent of the frequency with which validity proofs are generated by the L2 Operator.
The solution below can be applied to a broad range of use-cases, beyond Fast Withdrawals. Let us first describe the abstract need.
Blockchains allow trustless interaction between two parties, Alice and Bob. Alice may wish to publish a transaction that can be executed only when a certain conditional event occurs; Bob would like to execute Alice’s transaction once that condition has been satisfied, without having to get her approval again. We call the primitive that supports this specification, a Conditional Transaction (CT).
Implementing CT on L1 is straightforward, as a smart contract can enforce the coupling between the event and the transaction execution. When moving to L2 systems, this becomes a challenge. For example, in StarkEx, the signer passes the signed transaction to the operator, which is responsible for executing it, and allegedly nothing stops the operator from executing the transaction before the requested condition is met.
In this post, we focus on a CT specified on L2, that is dependent on an L1 event (i.e. L2 | L1). That is, the CT ensures the operator can execute a signed transaction only if some on-chain event occurred. Going forward, we will add a CT which depends on an event on another L2 event (i.e. L21 | L22), which will enable interoperability between StarkEx instances and StarkNet.
Below, we formalize the notion of such on-chain events and see how they can be used for CTs in StarkEx.
Introduction to Conditional Transactions
Registration of On-chain Events
CTs use Fact Registry contracts to keep track of on-chain events. In particular, a CT can not be conditioned on events unless they are registered in a Fact Registry. For example, if Alice transfers 1 ETH to Bob directly on Ethereum, there is no on-chain event that could be used as a CT.
In the example above, the Fact Registry contract would require a function transfer(), which Alice invokes with Bob’s address as the recipient parameter.
The transfer() does two things: (a) sends the transmitted ETH to the recipient, and (b) keeps a record of the transfer, for example storing a hash of the transfer parameters (sender, recipient, and amount) in the contract’s storage.
The Fact Registry also has an isValid() function, which receives a hash as a parameter, and returns a boolean — True if and only if it is a hash of a transaction recorded by this contract. The hash of the transaction (the transfer parameters, in our above example), is referred to as a Fact — representing the occurrence of the event. The process of introducing a new Fact to a Fact Registry is often referred to as fact registration.
An on-chain event signed on in a CT contains two fields (actually, their hash), (a) an address of a Fact Registry contract, and (b) a Fact that should be registered prior to executing the transaction.
StarkEx Conditional Transactions
StarkEx batches transactions, and settles them all on-chain with a single STARK proof. In case one of the transactions in the batch is a CT, StarkEx will ensure that the associated Fact is indeed registered in order to settle the batch; otherwise, the entire batch is reverted.
Examples To Conditional Transactions
In this section, we will demonstrate use-cases and how they can be implemented using CTs.
Detailed Example — Fast Withdrawals
In any L2 solution, the naive way to transfer funds between L2 to L1 is to finalize an L2 state update, including a Withdrawal Transaction on L1. In Validity Proof-based systems, like StarkEx, the finalization of an L2 state update happens when a valid proof attesting to it is accepted on-chain, which usually takes 10s of minutes. This means that if a user wants to move their funds from L2 to L1, they are forced to wait.
The purpose of Fast Withdrawals is to decouple this dependency, and allow users to trustlessly withdraw their funds to L1 in “blockchain-time”, i.e. within one Ethereum transaction.
How would this work? If Alice wants to withdraw 1 ETH from L2 to L1, Alice can sign a CT transferring 1 ETH to a Liquidity Provider (LP) on L2, conditioned on the LP transferring 1 ETH (minus some fee) to Alice on L1. Alice’s CT can be executed only if she first gets her funds on L1, therefore she faces no counterparty risk.
Let’s look at a toy Fact Registry contract that can facilitate such a transfer –
We can see that the contract has one payable function transfer() which does the two following things –
- Transfers amount of eth to address
- Registers keccack(amount, address, nonce) as true
The CT signed by Alice can only be executed if the Fact keccack(1 ETH, Alice, nonce) is registered in the Fact Registry. This Fact, in turn, can only be registered if the transfer of 1 ETH to Alice has previously occurred.
Alice was able to withdraw 1 ETH to her address trustlessly, and all it required was her signature and a single Ethereum transaction from the LP.
More Use Cases
A similar flow can capture the following types of events through an L2 CT transaction, for example:
- Alice wants to sell her 1ETH on L2 for 1000DAI on L1, if the price of ETH drops to 1010 DAI (as registered on-chain by a known oracle)
- Alice wants to give Bob 10ETH on L2, if Bob deposits 9.5 ETH under Alice’s name in a dApp of her choice (such as Aave or Compound)
- Alice wants to to give 10 ETH for Bob on DeversiFi’s L2, if Bob deposits 9.5ETH to Alice’s account in dYdX’s L2
The first use-case of CT is Fast Withdrawals, but StarkEx operators can implement many L2-L1 interactions using this building block.