Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC-1776 Native Meta Transactions #1776

Open
wighawag opened this Issue Feb 25, 2019 · 9 comments

Comments

Projects
None yet
3 participants
@wighawag
Copy link
Contributor

wighawag commented Feb 25, 2019

Simple Description

Native Meta transactions (a term first coined by Austin Thomas Griffith here : https://medium.com/gitcoin/native-meta-transactions-e509d91a8482) allows users that simply own ERC20 or ERC777 (ERC1155 could be supported too) tokens to perform operations on the ethereum network without needing to own ether themselves by letting a third party, the relayer, the responsibility to execute a transaction on the ethereum network carrying the desired operations (the so called meta transaction) in exchange of a token reward to cover the relayer's gas cost. They differ from traditional meta transactions (https://medium.com/uport/making-uport-smart-contracts-smarter-part-3-fixing-user-experience-with-meta-transactions-105209ed43e0) in that they only require support from the token contract itself and do not need the user wallet to be contract based.

The proposal here define the message format and token contract interface required for web3 wallets and browsers to provide a meaningful native meta-transaction display when users are requested to approve. This could even allow wallets/browsers to not bother users by displaying an ether balance when such users do not have any ether or when the application being used does not require it.

See EIP draft

Note : While in the current draft the ABI for the function that perform the meta transaction (called by the relayer) is not specified, it might be useful to include it so total gas estimation can be done by wallet/browsers.

Any feedback welcome.

@wighawag wighawag changed the title Native Meta Transactions ERC1776 Native Meta Transactions Feb 25, 2019

@wighawag wighawag changed the title ERC1776 Native Meta Transactions ERC-1776 Native Meta Transactions Feb 25, 2019

@PhABC

This comment has been minimized.

Copy link
Contributor

PhABC commented Feb 26, 2019

Nice!

Two initial comments:

  1. I would personally pack the gas receipt arguments in the _data byte arrays, allowing people to omit these arguments if not used and do something like this.

  2. I would suggest specifying somewhere that a signer can't be 0x0. ecrecover() can return 0x0, so a token that implements native meta-tx methods could see people generate signature on the behalf of 0x0, which could lead to undesirable consequences if 0x0 is considered a "burn" address.

@austintgriffith

This comment has been minimized.

Copy link

austintgriffith commented Feb 26, 2019

^^ Such good input. @PhABC, is this similar to your "Meta20" work? Do you smell a collaboration or should these two remain separate?

@PhABC

This comment has been minimized.

Copy link
Contributor

PhABC commented Feb 26, 2019

Yes, it's very closely related to the MetaTX ERC20 wrapper indeed. Ideally the ERC-20 wrapper can comply with this standard and vice versa. For those wondering what Austin is referring to, here's the relevant repository: https://github.com/horizon-games/ERC20-meta-wrapper. The native metaTX token contract is here.

@wighawag

This comment has been minimized.

Copy link
Contributor Author

wighawag commented Feb 28, 2019

yes we could easily add support for ERC-1155. it should work as ERC-777. We could simply a third EIP712 message type definition with the same data

  • I would personally pack the gas receipt arguments in the _data byte arrays, allowing people to omit these arguments if not used and do something like this.

Currently the standard as it stands to not preocuppy itself with the function that execute the meta transaction. If it did, I would like to know more about the reasoning behind the optionality of the arguments : which one, what would be the default values / behavior in each case ?

Would the signature scheme need to change ?
It seems to me that this won't the case as we could have them set to zero values (like the relayer field).

  • I would suggest specifying somewhere that a signer can't be 0x0. ecrecover() can return 0x0, so a token that implements native meta-tx methods could see people generate signature on the behalf of 0x0, which could lead to undesirable consequences if 0x0 is considered a "burn" address.

Good point, I ll add that

@PhABC

This comment has been minimized.

Copy link
Contributor

PhABC commented Mar 1, 2019

I want to discuss the current data passed related to the "gas payment" first, perhaps how this data is encoded in function calls is a different topic indeed.

gasPrice:

I would personally remove gasPrice, because that's something the relayer can choose on their own and is therefore not needed. If the tokenGasPrice is not enough for current market fee, they just won't execute the transaction. Otherwise, they can have their own transaction fee model.

gasLimit:

To keep, this is important.

tokenGasPrice:

I would rename that to gasPrice since I would remove the former gasPrice

relayer:

Should leave in as well.

gasToken :

I would add a field such that the user can specify how they will pay for gas: In WETH? In DAI? In WBTC?

baseGas:

I was once pondering if we should add a baseGas value, which is a value that accounts for gas not tracked within the MetaTX call, but that still needs to be spent by relayer. For instance, these costs include 21000 transaction cost, gas per byte of CALLDATA (function arguments), etc. However, it seems like each contract could hardcode it's own base gasCost that it would add when doing the reimbursement. This is not perfect, but it would remove the need for an additional field. Another way to deal with this would be to increase the gasPrice such that it covers the cost of baseFee. All in all, it's possible to avoid needing a baseGas fee, but I was curious if other thought the simplicity of this additional field would be worth it.

In our native meta-tx, we are currently using this as a "gasReceipt" struct:

// Gas Receipt
struct GasReceipt {
  uint256 gasLimit;             // Max amount of gas that can be reimbursed
  uint256 baseGas;              // Base gas cost (includes things like 21k, CALLDATA size, etc.)
  uint256 gasPrice;             // Price denominated in token X per gas unit
  address feeToken;             // Token to pay for gas as `uint256(tokenAddress)`, where 0x0 is MetaETH
  address payable feeRecipient; // Address to send payment to
}

Will probably remove the baseGas field however.

@PhABC

This comment has been minimized.

Copy link
Contributor

PhABC commented Mar 1, 2019

I'm also thinking of whether we should add a general data field in there as well, which could be used for additional logic. For instance, perhaps one wants to pay the gasFee via an ERC-721, ERC-20 or ERC-1155 token, which isn't really possible with gasToken alone. Perhaps some contract would like to add the baseFee that we removed. I'm guessing most of the time it would be 0x0 however, but in my case I know that in one of my use case I need to specify whether the gasToken is ERC-20 or ERC-1155. I'm just not sure what the best way to handle this is.

@wighawag

This comment has been minimized.

Copy link
Contributor Author

wighawag commented Mar 4, 2019

Hi @PhABC
thanks for the feedback.

gasPrice::

I would personally remove gasPrice, because that's something the relayer can choose on their own and is therefore not needed. If the tokenGasPrice is not enough for current market fee, they just won't execute the transaction. Otherwise, they can have their own transaction fee model.

I made this decision to include the actual ethereum gasPrice as a message field for 2 reasons :

  1. Unless the gasPrice is specified by the user, there is no incentive for the relayer to not reduce the gasPrice. If the user increase the token gas price it can't be sure the relayer will increase the gasPrice use for the transaction itself. gasPrice should be included so the signer is in control of the transaction speed.

  2. We should aim for full determinisim. gasPrice being a variable decided by the transaction signer, it can affect the logic of the call being made. While it is true that gasPrice is not usually used it could be.

gasLimit:

To keep, this is important.

Indeed, but unless it was clear enough, such gasLimit represent the amount of gas passed into the call (executed by the meta-transaction as part of the transaction executed by the relayer) so that the call is deterministic based solely on the input of the signer.

As such the meta-transaction implementation need to ensure enough gas is passed to the transaction so that when calling _to.call.gas(_gasLimit)(_data); the exact _gasLimit is passed in (solidity does not enforce this by itself and less could be passed in).
Also anything executed after that (gas refund tx...) need to have enough gas so the transaction does not run out of gas at the expense of the relayer. This extra gas is what baseGas would need to cover (see below)

gasToken :

I would add a field such that the user can specify how they will pay for gas: In WETH? In DAI? In WBTC?

Interesting, I actually did not thought about that because for our use case we did not plan to support such.
Thinking about it, thought I am not sure it should be included as part of this standard. It feels like it belongs to a different one where wallet can let user choose the token they wish to use to operate the transaction. This brings more questions and to me this should not be the responsibility of tokens to support it. After all we would only need one canonical implementation ( a la 820) and token that support native meta transaction would not need to implement it then as any application could use the canonical implementation instead (which is more likely to be already approved).

At the same time, the change is minimal and for contract that do not want to support different token, they would not even need to add that to the calldata.

Still, how wallet would know whether other tokens (non-native) are supported ?

Maybe the field could still be added but wallet should not think that they can let the user chose any token. So when an EIP712 signature is requested with the field gasToken specified, the wallet do not let the user change. On the other hand when it is not specified, it request the user to choose one.

This forbids the ability to suggest a default one though, unless the standard evolve to be a different RPC call (I liked the idea of being solely a EIP712 message definition, for several reason, one being backward compatibility).

So unless we can clarify the idea , my current stance is to not include it as part of this standard, that focus on native meta transaction.

baseGas:

I was once pondering if we should add a baseGas value, which is a value that accounts for gas not tracked within the MetaTX call, but that still needs to be spent by relayer. For instance, these costs include 21000 transaction cost, gas per byte of CALLDATA (function arguments), etc. However, it seems like each contract could hardcode it's own base gasCost that it would add when doing the reimbursement. This is not perfect, but it would remove the need for an additional field.

baseGas is indeed an important topic and that is what I was alluding to by the "extra gas required by the relayer". One thing is certain is that wallet/browser will need a way to know that value to display it to the user. I was at some point thinking of adding a mandatory function like baseGas() returns (uint256) that would let the wallet know what baseGas will be needed. We would need one for ERC-20 and one for ERC-777 , etc....

By the way, as mentioned about gasLimit, baseGas is not only the 21000 + calldata etc, but also the gas cost of gas refund transfer and other as the gasLimit specified in the signed message is about the gasLimit used in the call itself.

Another way to deal with this would be to increase the gasPrice such that it covers the cost of baseFee.

This is not that simple. a user could simply specify a high gasLimit to make believe the relayer that its margin will cover the extra fee, while the actual transaction consume very little.

All in all, it's possible to avoid needing a baseGas fee, but I was curious if other thought the simplicity of this additional field would be worth it.

Instead of including it in the message, I was thinking that the wallet should estimate gas of the relayer transaction to predict the actual amount of token being refunded. This would require us to define the abi signature of such function and is not ideal in my opinion as it gives you the total gas cost and can't predict the actual gas used when executed for real.

I currently like to have it included as part of the message to sign to avoid ambiguity and ensure the data is the same for everybody (wallet, user, relayer).

This does not allow the wallet/browser to figure it by itself though. But is that even desired ?

To summarize our 3 options are :

  1. adding meta_erc20_baseGas meta_erc777_baseGas
  2. adding baseGas field
  3. using estimateGas

1 and 2 together would :

  • allows wallet to display the value to the user
  • allows the wallet to compute it itself for generic application that might not know the value required
  • allows wallet to display the value when supporting only erc-712 and not ERC-1776

Having said that, I am not sure whether wallet need to be able to figure the value by themselves. At the end of the day, a relayer would be needed and such relayer would not accept random tokens. As such the relayer would be able to know what baseGas to use and the wallet would be able to know without having the smart contract to publicly tell.

As such the baseGas message field might be all what we need.

Note though that if baseGas is a fixed value (worst case value) the smart contract can avoid requiring it to be passed as calldata as such there is not much cost of baseGas being included in the message.

I'm also thinking of whether we should add a general data field in there as well, which could be used for additional logic. For instance, perhaps one wants to pay the gasFee via an ERC-721, ERC-20 or ERC-1155 token, which isn't really possible with gasToken alone. Perhaps some contract would like to add the baseFee that we removed. I'm guessing most of the time it would be 0x0 however, but in my case I know that in one of my use case I need to specify whether the gasToken is ERC-20 or ERC-1155. I'm just not sure what the best way to handle this is.

Hmm, if you only need to differentiate between ERC-20 and ERC-1155 you could do as the current draft do in regard to ERC777 and ERC20 : 2 different signature scheme with the same data

For instance, perhaps one wants to pay the gasFee via an ERC-721, ERC-20 or ERC-1155 token, which isn't really possible with gasToken alone.

I would say, let's focus on something concrete we have now. We could always create a new standard for wallet to support ERC-721... (as an extension)

@PhABC

This comment has been minimized.

Copy link
Contributor

PhABC commented Mar 5, 2019

Unless the gasPrice is specified by the user, there is no incentive for the relayer to not reduce the gasPrice. If the user increase the token gas price it can't be sure the relayer will increase the gasPrice use for the transaction itself. gasPrice should be included so the signer is in control of the transaction speed.

This doesn't really matter because the relayer can always wait before submitting the tx. If multiple relayers can execute, then this is no problem and if the relayer is enforced by you, you can change relayer if the service they provide isn't good. Think of exchanges like Binance that will withdraw your funds with a gas price of 50 Gwei to ensure good customer service. It also gives no room to the relayer to adjust the gas price if market changes, which reduces the chances that the tx will be submitted.

It feels like it belongs to a different one where wallet can let user choose the token they wish to use to operate the transaction.

Only supporting the current token makes the MetaTX market less efficient however, since now relayers are "forced" to accept the given token if they want to serve their users, instead of only accepting DAI or WBTC for instance. This adds complexity to the relayer and could fragment relayers. It's likely that smaller/new tokens would not be able to find reliable relayers as the risk for the relayers is too large. However, if users could specify DAI (or other common currency), then all MetaTX for all tokens with native metaTX are equivalent for relayers.

Still, how wallet would know whether other tokens (non-native) are supported?

Wallet just need to check token balances and check the approvals. Wallet could by default use the native token of the contract, which offers the same experience as your current proposal, but with the added flexibility that one could use another token to pay for gas.

As such the meta-transaction implementation need to ensure enough gas is passed to the transaction so that when calling _to.call.gas(_gasLimit)(_data); the exact _gasLimit is passed in (solidity does not enforce this by itself and less could be passed in).

Here again I would not enforce this per say and leave it to the relayer, where the contract reimburses up to the gas limit. A relayer will therefore have no incentive to exceed and the transaction will still go through if the relayer undervalues the amount of gas required. In your proposal, if gasLimit becomes insufficient, the tx is not-executable anymore, while in what i'm proposing, so long as there is a margin for profit, the tx is executable even if gasLimit is too low. Here again I personally think the flexibility improves the user experience and the probability that the tx will be executed correctly.

This is not that simple. a user could simply specify a high gasLimit to make believe the relayer that its margin will cover the extra fee, while the actual transaction consume very little.

Miners need to make the same type of decisions, where if a transaction gasLimit can be much higher than what is actually consumed. I don't think this will be a problem in practice with good enough tools.

Note though that if baseGas is a fixed value (worst case value) the smart contract can avoid requiring it to be passed as calldata as such there is not much cost of baseGas being included in the message.

That sounds reasonable. The simplicity of including a baseGas field for most implementations seems indeed worth it.

I would say, let's focus on something concrete we have now. We could always create a new standard for wallet to support ERC-721... (as an extension)

Without gasToken the current standard only seem to support ERC-20, but not ERC-721 nor ERC-1155, which is a mistake imo. I'm not sure what is the easiest way to generalize the standard however.

@wighawag

This comment has been minimized.

Copy link
Contributor Author

wighawag commented Mar 7, 2019

This doesn't really matter because the relayer can always wait before submitting the tx. If multiple relayers can execute, then this is no problem and if the relayer is enforced by you, you can change relayer if the service they provide isn't good. Think of exchanges like Binance that will withdraw your funds with a gas price of 50 Gwei to ensure good customer service. It also gives no room to the relayer to adjust the gas price if market changes, which reduces the chances that the tx will be submitted.

I am not convinced:

For the case when there is many relayers, indeed, the gasPrice used will tend towards the ethereum gasPrice equivalent to the tokenGasPrice value but then this is mostly equivalent to letting the user dictate the gasPrice.

If there is only one relayer (whether it was the only specified or not) on the other hand, the user becomes at the mercy of its decision. That relayer is incentivised to get paid more than what it is providing and so the user will wait longer than it should for its transaction. While we could argue that's the nature of such small market, my point is that the tokenGasPrice can still increase to offer incentive for such relayer to relay, on the other hand the actual ethereum gasPrice should be in the hand of the signer, so the relayer can't malicious grab the price and provide a slow experience. In such situation, the user can't simply increase the tokenGasPrice as he has no guarantee the relayer will increase teh gasPrice in exchange.
If the relayer is not broadcasting the transaction, the user can simply look for another one. This is the same in both case.

In other words, the only advantage a gasPrice determined by the relayer bring to the user is when the relayer (by potentially losing money, at least its margin) increase the gasPrice. The other way could only be to the detriment of the user.

As such for gas market price changes, which is only a problem for the user when the gas price is not sufficient anymore. We could allow the relayer to increase it but never make it lower than specified by the user.

This conflict though with the other issue arising from letting the relayer set the gas price: the user is not in control of the execution data anymore. In principle, I think it is important that the relayer should not have any say to the data being sent as part of the transaction. gasPrice, is such data.

I agree though that we could specify the need to not use gasPrice in receiver call as part of the standard. Personally I do not see any use case that would be restricted but is that a safe assumption?

if that is safe, I would agree to let the relayer increase the gasPrice but not decrease it.

By the way, there is another potential side benefit of a fixed gasPrice: it might help a network of relayers to coordinate since there would be no easy way for a late relayer to grab the price by submiting a transaction later.

Only supporting the current token makes the MetaTX market less efficient however, since now relayers are "forced" to accept the given token if they want to serve their users, instead of only accepting DAI or WBTC for instance. This adds complexity to the relayer and could fragment relayers. It's likely that smaller/new tokens would not be able to find reliable relayers as the risk for the relayers is too large. However, if users could specify DAI (or other common currency), then all MetaTX for all tokens with native metaTX are equivalent for relayers.

I think you convinced me on this one, not from this response specifically but from the general idea of generic meta transaction. This has some implications though and I am still ensure if they all fine. For example the current draft allow the transaction to specify a token amount to send to the destination. If we want to support that, we probably want to add yet another field tokenToSend but this also implies that we need a different way to support ERC-20 tokens. Approval is not transitive and as such the sender will need to either approve the receiver before hand (not great user experience) or the contract will need to transfer token first and let the receiver know of the token received (like a erc-20 tokensReceiced hook) and revert the transfer if the call failed (with all the gas cost it implies).

One thing worth noting here is that as I said we would only need one smart contract implementation for all native meta transaction, then. The current proposal could still focus on the message format, each field's role, receivers expectation and wallet support though.

Still, how wallet would know whether other tokens (non-native) are supported?
Wallet just need to check token balances and check the approvals. Wallet could by default use the native token of the contract, which offers the same experience as your current proposal, but with the added flexibility that one could use another token to pay for gas.

The question was about how the wallet know that the token supporting meta transaction support a specific token, not that the user approved the token. But now that I think a bit more clearly about such generic meta transaction contract, I can see that there is not reason the implementation limit which tokens are supported, this is up to the receiver (relayer and destination) to accept them or not.

Here again I would not enforce this per say and leave it to the relayer, where the contract reimburses up to the gas limit. A relayer will therefore have no incentive to exceed and the transaction will still go through if the relayer undervalues the amount of gas required.

Again by principle, the relayer should not have any say in the data passed to the call and this is even more important for gas than gasPrice.

But more importantly, if the relayer decide what amount of gas is passed to the call, when it fails, who is to pay the meta transaction? This should not be the user if the call failed due to the lack of gas.

In your proposal, if gasLimit becomes insufficient, the tx is not-executable anymore, while in what i'm proposing, so long as there is a margin for profit, the tx is executable even if gasLimit is too low

The only time the gasLimit would be too low in my proposal is when the user miscalculate the cost. It can't become insuficient.
To be more clear, in the current proposal, gasLimit is not the gasLimit the relayer need to send, it is the gaslimit sent as part of the call. It is up to the relayer to provide enough gas to reach the call executing with enough gas to complete the transaction. If the relayer do not, it lose its gas.

As such if ever the user sign a gasLimit too low, the relayer (provided it sent enough gas for the whole tx) will be rewarded for the execution anyway (and the meta nonce increased) since we can calculate that it was the user's mistake not the relayer.

Miners need to make the same type of decisions, where if a transaction gasLimit can be much higher than what is actually consumed. I don't think this will be a problem in practice with good enough tools.

That's not a fair comparison. In the miner case, there is not extra margin decided by the miner The miner simply knows that they will be paid x for y gas.

Without gasToken the current standard only seem to support ERC-20, but not ERC-721 nor ERC-1155, which is a mistake imo. I'm not sure what is the easiest way to generalize the standard however.

As said about ERC1155 we can easily add ERC712 specific message preamble in the TYPEHASH.
We could standardize it like <ERCXXX>MetaTransaction(... and the gasToken field would define the id.

I' ll add that to the proposal.

We could also simply add a string field instead that state the ERC to be used like : tokenERC. I personally prefers the preamble but I would not mind changing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.