Gas efficient contract architectures for escrowing assets

In a state channel network, multiple channels need to be funded on chain — be they user-to-user or user-to-hub connections. There are a few approaches that we know of:

  1. Escrowing assets in a singleton contract for multiple channels
  2. Escrowing assets in an ethereum account per channel.
    a. Where the account is a contract
    b. Where the account is not a contract

From a gas point of view, the main pros and cons are as follows:

1. Singleton escrow contract

  • No contract creation costs are necessary per channel — the single contract need be deployed only once (per chain).
  • Escrow of ETH and tokens is relatively expensive and cumbersome. Each deposit must update some storage in the escrow contract in order to declare which channel is being deposited into: in other words, a “layer 2 accounting system” needs to be implemented in the escrow contract.
  • This further implies that deposits must be made via a contract call to the escrow contract. If the asset is an ERC20 token (far and away the most popular standard), this necessitates i) an approve call to the token itself followed by ii) the deposit call to the escrow contract (which internally calls transferFrom on the token, and atomically updates the layer 2 accounting system). This flow causes excess reads/writes to storage in the token contract (e.g. the allowances mapping).
  • Depending on how the layer 2 accounting system works, the costs can be partially mitigated by clearing storage slots when the channel is liquidated, but only so long as the relevant gas refunds remain a feature (see EIP 3529).
  • Yet a further nuisance is the need to perform i) and ii) above in two separate transactions, implying poor UX due to longer waits and higher gas overheads due to the base transaction fee.

2a. Escrow contract account per-channel

  • Contract creation and code deposit are gas expensive. This can be partially mitigated by having the contract be a very lightweight proxy such as an EIP-1167 Proxy. The proxy points to a singleton “Master Adjudicator” which need be deployed only once (per chain). The costs can also be partially mitigated by SELFDESTRUCT gas refunds, so long as they remain a feature of mainnet (not for much longer, see EIP 3529 scheduled for London).
  • Contract addresses deployed using CREATE have difficult-to-predict addresses. This is solved by having the escrow contract deployed with CREATE2. There are benefits to having predictable escrow contract addresses: deployment can be delayed until the funds need to be liquidated, adding some extra privacy and security.
  • Escrow of ETH and tokens can be as cheap and easy as is possible: just send the assets to the escrow contract for the appropriate channel.

2b. Escrow (non-contract) account per-channel

  • This architecture would be made possible by EIP 3074. See this comment.
  • Escrow of ETH and tokens can be as cheap and easy as is possible, just send the assets to the escrow account (called a synthetic EOA) for the appropriate channel
  • A singleton Invoker contract controls all of the synthetic EOAs. There is a unique synthetic EOA address for each channel
  • No layer 2 accounting system is necessary
  • No contract creation or code deposits are needed
  • It’s the best of both worlds, having our cake and eating it too!
  • There are some modest additional gas expenditures for AUTH and AUTH_CALL when liquidating the channel.

More detail on synthetic-EOA-per-channel approach
  1. A singleton Adjudicator contract is deployed ahead of time by the state channel protocol team, and plays the role of an EIP-3074 Invoker.

  2. Then, state channel participants generate a 32 bytes channelId in the established way: they hash the combination of their state channel wallet addresses with the chainId and a nonce.

  3. The participants then derive a channelAddress from the channelId. They do this by interpreting the channelId as an EIP-3074 signature, i.e. a ECDSA signature on the digest:

digest = keccak256(MAGIC || padTo32Bytes(Adjudicator) || padTo32Bytes(0))
channelAddress = ecrecover(digest, syntheticSignature(channelId))

Note that we set the EIP-3074 commit to 0.

  1. Participants then send funds directly to channelAddress, for example via ERC20.transfer . No approve transaction is necessary.

  2. When it comes to liquidating the channel, any participant holding a “support proof” (state channel jargon for the requisite off-chain state and signatures to close a channel) may submit that proof to the Adjudicator. It will

    1. performs all of the usual checks according to the state channel protocol;
    2. do auth(commit = 0, yParity = 0, r = channelId, s = channelId) , which sets msg.sender to channelAddress;
    3. do authcall to interact with token contracts and move tokens from channelAddress to the beneficiaries of the channel.

Note that channelAddress is a “synthetic EOA”, meaning an EOA for which the private key is not known or used. In that sense it is similar to a contract; but a crucial difference is that no code or storage needs to (or even can) be deployed “under” the channelAddress.

This is because the “synthetic signature”, in this case the channelId, confers the Adjudicator the power to control precisely one Ethereum account: namely, the account with the channelAddress address. I’m glossing over some details around how to construct a synthetic signature from the channelId – we can discuss that over at Reliable and safe generation of synthetic signatures.

The term “synthetic EOA” is somewhat oxymoronic: the account is not externally owned by any single party, or even by a set of parties. It is controlled by a single contract only.

The collision resistance of the signature scheme makes it infeasible that any other contract can control the synthetic EOA.

Now it is important for funds in a state channel escrow to be liquidated only when strict conditions (set by the state channel protocol) are met. It would be possible in principle to make use of the commit field in the AUTH message format, to commit to the relevant cryptographic proof for liquidating the channel. Alternatively, the commit field is set to zero, which gives the maximum level of control to the Adjudicator.

Since the Adjudicator must be trusted (in the sense of having its behaviour verified or audited), there is no real change in the trust model by making it an EIP-3074 Invoker and handing it complete control over a set of synthetic EOAs.