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