Brainstorming the token standard in eth2


#1

The following is a brain dump of my thoughts so far; at first glance I’m leaning toward option 3, though all options are imperfect.

Tokens in ETH2

There is a desire to make a new ERC standard for tokens in ETH2, which addresses some of the challenges of the current standard in ETH1. Particular issues with the standard in ETH1 include:

  • Severe UX losses from needing to send multiple transactions when using a dapp that requires payment in tokens (eg. selling tokens in Uniswap): first one must send a transaction to approve the target contract’s right to withdraw tokens from the sender’s account, then one must call the target contract, at which point the target contract can then “pull” tokens from the sender’s account
  • The difference between the workflow with ETH vs the workflow with tokens, forcing contracts trying to keep themslves simple to rely on “WETH”, a wrapper around ETH that makes it conform to the same ERC20 standard as tokens

Desiderata for an improved standard include:

  • No specific EVM-native concept of “tokens”
  • Maximum similarity between the workflow of dealing with ETH and dealing with tokens
  • Easy extendability to “weird” types of tokens, eg. it should also be easy to make contracts that accept payment in NFTs, ENS domain names, etc
  • Maximum simplicity
  • Preserve the property that if a call that requires tokens for payment is reverted, the token transfer itself gets reverted
  • Allow calls with multiple tokens needed for payment (eg. think addLiquidity in Uniswap for an example of where this is needed/useful)

Option 1

Use something very similar to ERC20 in ETH2, and make ETH itself an ERC20 contract.

Pros:

  • The multiple transaction requirement issue does not exist in ETH2, as abstracted accounts can make multiple calls within one transaction
  • Familiar dev workflow

Cons:

  • Extreme space and Merkle proof size inefficiency (transactions using ETH will need to call two state objects instead of just one, and we’ll need to store two objects instead of one)
  • If a token contract’s execution fails, the transfer gets reverted but the approval does not

Option 2

A “dependent call” feature where a user can make N calls that execute consecutively, where the last call can see the receipts generated by the previous calls, and the last call failing causes all calls to revert.

Example workflow:

uniswap_mkr.tokenToEth(
    eth=10,
    dependencies=[mkr.transfer(to=toaddr, value=50)]
)

In the Uniswap contract:

def tokenToEth(eth: int):
    assert len(dependencies()) == 1
    dep = dependencies()[0]
    assert dep.target == self.mkr_address
    assert dep.dest == self
    tokens_sold = dep.value
    ...
    

We can then make call and send entirely separate operations, except that send also returns a response ID; so sending ETH would also use the exact same workflow.

For the add_liquidity feature, two dependencies would be required:

uniswap_mkr.addLiquidity(dependencies=[
    mkr.transfer(to=toaddr, value=50),
    send_eth(to=toaddr, value=10)
])

In the Uniswap contract:

def addLiquidity():
    assert len(dependencies()) == 2
    ethdep = dependencies()[0]
    assert ethdep.target == SEND_ETH
    eth_provided = ethdep.value
    tokendep = dependencies()[1]
    assert tokendep.target == self.mkr_address
    tokens_provided = tokendep.value
    ...
    

In the VM, this would look like an EEI entry with identical properties to CALL, except with room to make multiple calls.

Pros:

  • Allows arbitrary dependencies between calls to be expressed
  • Similar “push” style to sending ETH today

Cons:

  • Requires adding a new and relatively complex instruction to the EEI
  • Possibly unintuitive with new/unknown security issues

Option 3

Use the approve and transferFrom workflow from ERC20 and extend it to ETH without making ETH itself an ERC20. That is, ETH-bearing calls would be banned, and instead we would have (i) pure ETH sends, (ii) approve and transferFrom operations for ETH.

Example workflow:

mkr.approve(uniswap_mkr, 50)
eth_approve(uniswap_eth, 10)
uniswap_mkr.addLiquidity(tokens=50, eth=10)

In the Uniswap contract:

def addLiquidity(tokens: int, eth: int):
    mkr.transferFrom(msg.sender, tokens, self)
    eth_transferFrom(msg.sender, eth, self)
    ...

A system contract eth could be added at some address, eg. 0x10, with eth.transferFrom being an alias for eth_transferFrom and likewise eth.approve for eth_approve; this allows ETH to be referenced as a token with a contract address like any other token.

Pros:

  • Harmonizes ETH and ERC20s by making ETH ERC20-like but without bloating state or transactions

Cons:

  • Requires the complexities of the current “approve and transfer” workflow, plus VM rules for making it gas-efficient
  • If a token contract’s execution fails, the transfer gets reverted but the approval does not

#2

Should the interface remain ERC-20 compatible?

If we added nonce management to the token, then we could provide a signature to uniswap that could authorize a mkr.transferFrom(msg.sender, tokens, self, r, s, v) within one transaction. I am not sure of the additional cost for providing and validating the signatures in addition to tracking the nonces :scream_cat:

Doesn’t address the point “Maximum similarity between the workflow of dealing with ETH and dealing with tokens” :thinking:


#3

Option 1 and option 3 feel natural to me from a development / logistics standpoint. So I prefer those and am short option 2.

Similar to this issue, but a bit separate, a big issue imo is that paying fees / gas is coupled with a transaction itself in eth 1.0. It means I can’t do things like (for an arbitrary transaction w/ a contract that may not support this natively) pay someone in dai and have them pay the eth gas fee for the transaction itself. It’d be cool if this is part of eth 2.0 (or is it already part of eth 2.0)?


#4

I can’t do things like (for an arbitrary transaction w/ a contract that may not support this natively) pay someone in dai and have them pay the eth gas fee for the transaction itself. It’d be cool if this is part of eth 2.0 (or is it already part of eth 2.0)?

You can do this as a layer 2 technique. Two ways I’ve thought of so far:

  • Have a “wrapper” mechanism where a transaction can “contain” other transactions, and the inner transactions pay DAI to the outer transaction and the outer transaction pays the ETH fee to them
  • Accounts can have a “poking” mechanism where if you don’t have enough ETH to pay one transaction, anyone can poke them to sell some of their DAI on Uniswap to get more ETH.

If we added nonce management to the token,

Adding signature and nonce management to the token sounds to be like a very bad idea. It’s mixing application functionality with authorization functionality. The better thing to do if we want the ability to pay gas on other people’s behalf would be to add the transaction wrapping mechanism I described above.