Brainstorming the token standard in eth2

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
4 Likes

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:

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)?

1 Like

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.

1 Like

I would highly suggest that ETH2 have no hard-coded specification for tokens, and work like ETH1 in that regard. Just allow arbitrary token contracts be created and have standards come out of that. That way, everyone can handle this how they want, permissionlessly. Otherwise, ETH2 would be strictly worse and would force people into a confined box. Adding some nice added arbitrary functions like vitalik suggests is a good idea!

Also instead of the ‘poking’ mechanism, why not allow execution to be paid for using ERC20 tokens directly? Allow the validators to accept TX fees in tokens, if they want, arbitrarily. Hell you can even treat ETH like a token (weth) to make it all simpler.

2 Likes

@vbuterin @joeykrug

This is already possible today and we have fully implemented it. You can use e.g. Augur und you never have to have any ETH on your account. You are signing meta-transaction and you also authorize a token (e.g. DAI) payment to the “outer transaction” that puts your transaction on the blockchain. By the way - the same concept will also make it possible to do a “approve” + “spend” within one atomic transaction.

https://safe.gnosis.io

By the way - nothing here seems eth2.0 specific. Those ideas have been discussed as alternative token standards for ETH 1.0 as well. I suggest to not mix up those standards with base level changes - or is there something I am overlooking?

2 Likes

Somewhat OT:

“Fixing” this could be achieved differently, by changing no-destination transactions (with to=='') to have ADDRESS of the EOA (and likely fall-out changes to CREATE and CREATE2). We could then use such transactions for things other than contract deployment and graffiti.

Currently (quoting “transient” programs out of context, where I’ve touched on this briefly):

Haven’t pondered much on whether there are concealed invariants here that might get broken. SSTORE during this lowest-level call frame might become problematic, for example.

I am also not a fan of integrating tokens as a special unit into the core protocol.
All of those problems can be solved in a second layer

ERC20 as we proposed it back then, is broken. I was never a fan of the whole approve transferFrom logic. It is cumbersome and complicated.
ERC777 is in my opinion the right approach, by making smart contracts registering themselves to be able to receive tokens, we are back to a one transaction process.

Meta transactions are a super important topic, but not sure if it needs to include payback up front, there could be many off-chain payment methods involved that pay for relaying transactions. So it should be rather an addition, not a requirement for meta transactions.
IMO ERC725 can solve most of the UX problems we have today, by abstracting account away from keys and make the 725 proxies the real accounts (again this is only layer 2, nothing that needs to be part of ETH2.0 core). Your flow is then: private key -> key manager/multisig -> 725 proxy -> any other smart contract.

Here the key manager could also accept meta transaction with all kind of pay-back or non-payback functionality.

So my opinion: Option 4, better token standard like ERC777

3 Likes

To play devil’s advocate to this discussion, it seems like everyone’s on the “integrating the concept of tokens into the base layer is bad” train. Here are a couple pros of allowing tokens in the base protocol:

  1. Tokens are just ownable/transferrable state issued by some originating party. They form a vast amount of the current state of Ethereum. If we want to do something with Storage Rent/Fees, it’s much more complicated to do with centralized “token ledger”-style contracts (a la ERC20) than it would be for individually-owned token entries in the account’s “balance trie”.
  2. Tokens that have their own state trie can be optimized for efficiency purposes. If there are only a few ways to store them, and a few ways to transfer them, it makes it easier for client implementers to make assumptions for optimizations because these assets are far more limited in scope than general-purpose storage slots controlled by general purpose smart contract code.
  3. Security analysis gets a lot easier. Since different opcodes are used to interact with “important” pieces of states (or “assets” as defined by the developer) than with general state entries (things that basically manage application flow), it is easier to see when a security invariant is broken that may economically impact users.

I really think it would be much easier to handle in-protocol. We already have a sort of example of this in practice, as Ether balance is managed per the account, and is not an entry of the account’s storage trie. Just my 2 cents because everyone wants Ethereum to be as general-purpose as possible because it can solve all the things, but sometimes a little bit of specialization can alleviate a lot of pain. I think at this point we all know that Ethereum is great for managing digital assets, so I think this is a good level of abstraction to make.

2 Likes

Maybe I’m missing something, but I don’t see where Vitalik suggested integrating token standards as a core part of Eth 2.0. I think his original post makes a lot more sense after reading this new proposal for a minimal execution layer for phase 2: https://ethresear.ch/t/a-layer-1-minimizing-phase-2-state-execution-proposal/5397.

I like option one and three. It would be great to have a single interface for interacting with transferrable assets, but the multi-tx workflow should be improved. I do think three makes a little more sense, because there is a difference between ether and other tokens.

Assuming option 3, could one of the following remove the need for multi-tx workflows?

  1. msg.value could be extended to work for an arbitrary token interface. If a transaction is otherwise valid, an op code ReceiveMsgValue could be added to access a special function (only callable using this opcode?) on the token contracts which bypasses the approval process. I’m sure this idea can be improved, but the idea is that by signing this tx the user has implicitly stated they wish to approve the destination contract.
  2. approve(address, amount, tx, signature) could bounce the transaction to the appropriate destination after adding the destination to an approved list. The destination would verify the original sender and their tx with the supplied signature.
  3. approve(address, amount, signature) could be called from the destination contract with a signature that the tx.origin user supplies. After successfully approving itself, it can continue transferring the funds.

One thing that ERC-777 brings that I suspect will be very popular is the ability to operate on another token holder’s behalf, and it hits a common theme.

There seems to be a generic “authority to act” requirement: multi-sigs, metatransactions, etherless operations etc. are all examples of this but I wonder if with eth2 we can do a little better than with eth1 and build a proper authority system.

In eth1 there is a single authority system: the public key of the signature of the transaction must equal the address as which the transaction acts. This is so obvious that transactions don’t even bother to hold the address separately, deriving it from the signature, but what if this wasn’t the case?

What if an address could have a different authority? For example, it might require a multisig such that if a transactions is presented to the network with authority signatures of 2/3 of the multisig participants the network would run the transaction as that address.

[I use “authority signature” to discern from the existing “transaction signature”. Every transaction would retain a transaction signature and that is the authority for the network to take the gas from the transaction signer. Some authorities would not even need signatures; a simple time-locked authority, for example, would not need any data].

We can do this today with smart contracts, of course, but it’s ad-hoc: there is no standard way of even doing a simple multisig, let alone some of the more esoteric authorities that could be dreamed up. It would be very handy indeed (and would remove some of the issues around the multi-step approve/spend patterns) if authority could be baked in closer to the core.

Of course, lots of questions remain. Who defines authorities, how are they defined, does it cost gas for an address to set an authority, does it cost gas to check the authority, can multiple authorities interact, and many more. But if done right authorities could add a heap of base-level functionality, remove lots of redundant (and probably badly-written) code scattered around smart contracts, and simplify a lot of process flows that are currently painful, to say the least.

(And to throw another option in the original mix: how about if (fungible) tokens became first-class currencies alongside ETH?)

I can see I confused people quite a bit; I am NOT advocating a first-class token system. Rather, I am suggesting the idea of having an ERC standard for ETH2 parallel to (but ideally better than) ERC20, and then designing ETH2’s concept of ether to itself be compatible with that standard.

This is exactly what “account abstraction” is supposed to be about. Now I suppose one special thing that should be standardized is the idea that if you want to use an account to authorize things off chain (the “sign in to websites with ethereum” use case), then there should be a standardized way to verify that. This could mean requiring accounts to have a special function verifyAuthorization, or it could mean signing a transaction that performs a function call with some data to some dummy address with a zero gasprice.

That said I definitely disapprove of making signature an argument to approve as a way of handling token payments to contracts, because it fails to cover the use case where the thing approving is not an external user (and instead is a contract or DAO or something similar).

This is already possible today and we have fully implemented it. You can use e.g. Augur und you never have to have any ETH on your account. You are signing meta-transaction and you also authorize a token (e.g. DAI) payment to the “outer transaction” that puts your transaction on the blockchain. By the way - the same concept will also make it possible to do a “approve” + “spend” within one atomic transaction.

The main challenge with using this today is that there aren’t standardized p2p networks where you can publish these transactions and expect them to get wrapped and included. If this could be standardized that would be really great (and also good for privacy as we can finally work around the gas fee payment deanonymization issue on all present-day mixer contracts!)

1 Like

This is really an important point, especially given that a huge percentage of all transactions are token transfers with identical input formats and generated events. I’m guessing is more than 2/3 of all activity (not verified). If an implementer knows that a certain transaction is a token transfer, she can optimize the shit out of the way it’s stored both in memory and on disc.

This is also quite true.

1 Like

And I’m not even talking about a lot of specialization.

ERC20, for example, can be defined as a standardized unsigned integer balance and allowance, with arbitrary but consistent transfer authentication rules, arbitrary but consistent issuance rules, and a few other small metadata parameters such as total issuance, decimals, ticket, etc. That’s a lot of leeway for storage optimization, and it doesn’t even have to be mandatory.

Allow alternative use cases, but make the common use cases very cheap, such that issuers are incentivized to adopt them… That’s a very powerful mechanism at the protocol layer.

It can still be a full turing-complete vm under the hood, but with shortcuts for common use cases that we have built good optimizations for, reducing burden on the network in aggregate.

1 Like

I’ve been thinking more about the approach of extending msg.value to support arbitrary tokens and I’d really like some feedback on the idea:

msg.value is extended to be the following object:

{
  "amount": uint256,
  "token": Address,
}

Now let’s imagine user A wants to send tokens to contract B. User A crafts a transaction using the new msg.value definition and sends it to contract B’s deposit function:

contract B {
  mapping(address => mapping(address => uint256)) deposits;

  function deposit() payable {
    deposits[msg.sender][msg.value.token] += msg.value.amount;
    get_msg_value();
  }
}

The deposit function updates its records and calls get_msg_value(), which is defined as an opcode or EEI host function.

function get_msg_value() {
  token = GenericTransferableTokenInterface(msg.value.token)
  delegatecall(token.transfer(self, msg.value.amount))
}

The msg.sender and msg.value of the call to contract B is forwarded to the token contract using a delegate call and self is the address of the current execution context, contract B in this example. By nature of signing and sending the original transaction, user A approved the transfer to contract B.

I’ve been thinking about this a little more, specifically how a token contract may look in a world of state rent.

If we take state rent as a given then an account needs some way of ensuring their token holdings are protected from being orphaned due to lack of rent payment. We also need to avoid griefing, where either an account or a contract is lumped with high rent fees due to third-party spamming (e.g. malicious airdrop). There are two ways of associating state with an (account, contract) tuple that I can see:

  1. The account creates an amount of storage for a given contract
  2. The contract creates an amount of storage for a given account

In either case the contract is the only entity allowed to write to the storage. It is possible for any party (i.e. doesn’t have to be either the account or the conract) to pay rent for a specific (account,contract) piece of storage.

The benefit of this is that it allows fine-grained control of rental payments so accounts don’t end up paying for what they don’t want to keep, and there is no reliance on (possibly bloated) contract storage to retain the state they need. The major downside is the confusion that it could cause when a user is potentially faced with a list of thousands of (contract,size) tuples about the storage they currently rent and the decision making they need to retain the ones that matter.

This gives more flexibility than a standardised slot in storage, and allows for future innovations without requiring changes to the model. It would also play nicely in to data sharding, as the (account, contract) tuple would allow mapping to the relevant shard.

This would also appear to be useful as a general model of storage, as it ensures accounts retain ultimate ownership of their storage for all contracts. There would need to be a way of contracts accessing this storage, and I’m sure there are lower-level problems I haven’t thought of, but I’d be interested to hear if this is something that makes sense (in which case I’ll think more about how a token standard would look with this model).

[Note if a particular piece of storage shouldn’t be owned by the account it doesn’t have to be, so if for example a contract recorded a negative opinion of an account the account couldn’t arbitrarily remove it by not paying rent.]