EIP-3074: AUTH and AUTHCALL opcodes

Sure thing! Is there something specific you’d like to see beyond just “it’s exactly like call, except…”?

I had separated it out so that signed parts of the message go in memory and the unsigned parts go on the stack. It also has the nice property that introducing a new type down the road is pretty flexible. I think but am not 100% sure that copying the TLP out of calldata into memory should be reasonably efficient.

Happy to change it though!

I wholeheartedly agree. Not sure how to avoid talking about them though. You’ve got the sponsor/sponsee and I don’t think you can avoid mentioning either of them. The invoker contract has to exist, and so does the call destination… Is there some other data I can trim out?

There should be types for all of the ABI encoded fields (eg. type: uint8), and if I missed any that’s a problem!

Were there any other unclear points I can tackle?

Thanks for the feedback, I appreciate it.

Just want to note one other suggestion that I’ve received:

Instead of signing over the calldata, mingas, value, etc. only sign over keccak256(type || abi.encode(invoker, chainid, nextra))

This has a certain elegance to it, and since you already have to trust the invoker for replay protection, why not trust it for everything?

This would allow multiple calls with different calldata (ex. approving the Uniswap Router, then performing a swap, and finally doing the sponsee’s transaction) with a single signature.

The downside is that a broken/vulnerable/malicious invoker can have nearly total control over an EOA, and writing a safe invoker will take a significant amount of care.

(Thanks to @adietrichs and Chris Buckland for independently suggesting this change.)

You could use abi.encode(uint8(...), uint8(...) and so on explicitly. Also - note that abi.encode always pads value types to 32 bytes, so v is padded - is that intentional?

With respect to presentation: Maybe it would help to say something like “functions like call to address x with value y and arguments arg, where x = … , y is subtracted from the balance of … and arg is …”

It would also be nice to clarify the call depth (it is not a call to the new sender and then a call to the actual destination, but just a single one) and that no nonce is incremented.

1 Like

The current execution context is static (i.e. STATICCALL) and value is non-zero

I disagree here. Let’s say were at depth 5, which is a static context.

If I try to do a CALL(x, x, 1, ...), then I will drop back to depth 4, not get a “failed call” on the stack.

The same should apply here, doing a SPONSOREDCALL(x, 1, ..) should drop me back a level.

TLDR; the error should be in the earlier context, not the current context. The semantics of STATICCALL are clear, and this particular op should not have any particular relation with that operation.

I also maybe think the stack format should be g,a,v, ... where a is the sponsee. The gav-order is used by every other calltype.

I put together an example of an invoker that we may use for ITX (Infura Transaction Service):

Generally speaking, when going through the example, I feel the opcode needs an additional return value of returnedData to keep it consistent with the other calltypes.

We should rename calleeSuccess to opcodeCheck (or something better) and then change the meaning of success to mean the CALL was successful (e.g. the internal transaction) as that keeps it consistent with the other CALL opcodes.

In the EIP, it mentions that it is worth checking if sponsor != sponsee to protect currently deployed reentrancy guards msg.sender == tx.origin. I have implemented that as tx.origin != signer.

The current EIP does not say anything about that, but I understand it originally did. Could someone elaborate on what the problem is?

I would like to see some more fleshed-out usecases and examples. Like these:

  • A user A who holds XToken (but no eth) wants to send 10 of them to an exchange (and is willing to pay 1 XToken to the sponsor).
    • How is that accomplished ?
    • And how can A prevent that someone else, B replays his blob and sends 10 more to the exchange (if only for shits and giggles, while not making a profit).

Aside from that ^ usecase, what is a practical usecase where a sponsoredcall would be used?

Some contracts (I don’t have an example offhand) have deployed a reentrancy guard: tx.origin == msg.sender to force contract calls to only originate from an EOA.

In EIP-3074, the tx.origin and the signer can be the same address. This allows an attacker to bypass the re-entrancy check as the immediate caller is a contract and not an EOA.

(Sorry, previous post had != instead of ==).

Ah. Not as currently written. The ORIGIN remains the EOA that paid the eth, and the SENDER becomes the one who signed the message. So the check remains functional (fwiw)

EDIT: Yes, I see. The current EIP does say, in the security section:

Checking msg.sender == tx.origin no longer prevents reentrancy. Adding the pre-condition that sponsor != sponsee would restore this property.

I somehow missed that part :slight_smile:

It is useful whenever Alice (sponsor) wants to pay the network fee for Bob (sponsee).

There are several reasons why Alice may want to send it for Bob:

  • ERC20 fees. Alice pays the network fee in ETH and Bob refunds it with ERC20 tokens.
  • Stuck transactions. Alice wants to improve the user experience for Bob, so she takes of sending the transaction for him. He just hits “send” and the rest is taken care off.
  • Bidding in fee market. Alice can take care of sending the transaction and competing in the fee market for Bob. She optimises for paying the best price at any given time (whereas Bob may just hit “bump fee” and overpay).

Both wallet providers and dapps are the big benefactors for the EIP.

  • Wallet provders. Better user-experience as the wallet takes care of sending transactions for their users. It also allows users to pay for the fee in an ERC20 token. Examples include Argent, Dharma, Gnosis, Staker.app, etc. All services require a wallet contract as that is the only way to make it work today. (Some providers need to pre-farm wallet contracts to make it affordable for their users)

  • Dapp websites. One of the most common complaints that dapps get is “stuck transactions” / “no eth to send”, etc. This type of EIP would let websites take care of sending transactions on behalf of their users.

There are some companies that offer transaction relay as a service. Notably Infura Transaction Service (that I work on) that would benefit for the introduction of the EIP. The biggest pain point is that we need to recommend users 1) use wallet contracts or 2) enjoy a janky experience due to ERC20 tokens not supporting meta-transactions.

That one sounds a lot like that usecase I wrote above. And I can’t see how Bob is protected from replays?
For all of these cases, I’d like to understand the whole process, basically flow-graphs of what parties/contracts would have to exist, and what the whole UX would be.

EDIT:Ah, the invoker is what prevents the blob from being arbitrarily replayed in my example.

You can see another example of replay protection in @adietrichs’ example: https://gist.github.com/adietrichs/ab69fa2e505341e3744114eda98a05ab#file-eip3074relayer-sol-L77

I’d like to interject to get a temperature check on only signing over invoker and nextra instead of what’s currently in the EIP. I’m leaning towards that direction, and I don’t want to waste everyone’s time reviewing something that might change.

The problem for the sponsor (A in your example) is the bounty-reward problem:

If Alice is due to collect an on-chain bounty upon the successful execution of a transaction, then generally speaking there is always a risk that her transactions fails because the bounty was already claimed.

The classic example is that Bob simply spends the ERC20 tokens while Alice’s sponsored transaction is in-flight. Unless you have a special contract set up, then generally speaking, Alice will just waste some gas. Several wallets already offer DAI payment as a feature (Argent) and so far they just absorb the risk/cost of it.

For the use cases. It really depends on:

  1. Payment mechanism. How is the sponsor being paid for sending the sponsees transaction?
  2. On-chain authentication. How is the sponsee authenticating with the target contract?

I should stress that both payment and on-chain authentication are separate problems. The EIP is only focusing on on-chain authentication.

For a full picture, the payment mechanism can be pre-payment (ITX) or the bounty approach (on-chain refund).

How prepayment and receiving job may work.

Figure 1: Overview of Gas Tanks for ITX

In Figure 1, we only consider the pre-payment approach and we assume the clients have a pre-paid gas tank with ITX. They may have paid in ETH or FIAT, but in the end they will have an ETH-denominated gas tank with the service.

The client can simply send a signed transaction to the API:

{
"id": 0,
"jsonrpc": "2.0",
"method": "relay_sendTransaction",
"params": [
   {
      "to": "0x0000000000000000000000000000000000000000",
      "data": "0x123456",
      "gas": "100000"
   }, <signature here>
]
}

ITX takes the “to”, “data”, packs it into an Ethereum Transaction and then sends it to the network for them.

The next problem, which is really the focus of EIP-3074, is how the user authenticates with the target contract that will be executed:

  • Modified target contract. It authenticates the user with a signed message instead of msg.sender.
  • Wallet contract. An intermediary contract holds the user’s funds and verifies their signed message. It then .calls() in the target contract and acts as their identity on the network.

The former approach is pretty janky as it doesn’t work with most ERC20 tokens or most contracts for that matter. There is some work towards standards such as EIP-2771, EIP-2612 and EIP-3009 standards, but they all fall short due to lack of adoption.

As a result, most services need to deploy wallet contracts. An example of how to execute (and the refund via bounty) can be seen with Gnosis Safe, but asking users to both deploy expensive wallet contracts and move all their assets to it… is typically a blocker to adoption.

I hope the above example helped. Sorry, it was the quickest thing I could think to put together and I hope it isn’t too long!

1 Like

From the rationale section:

Including sponsee in the arguments to TXCALL is a gas optimization. Without it, invokers would have to do their own ecrecover before calling into TXCALL to verify/adjust any state for replay protection.

While I think this is generally sensible, it does make invokers that don’t care about the sponsee address somewhat less efficient. I think the pattern of trusted invokers (think e.g. an exchange collecting user deposits, or a user with multiple EOAs - any time the sponsee fully trusts the sponsor) could reasonably become a significant one, so there might be a case for optimizing for it.

The simplest way to do so that I can see would be to also allow the 0 address as a valid sponsee opcode argument, to signal no requested address validation. Given that Solidity would likely abstract that logic away anyway, I don’t think there is a high risk of this leading to any confusion. The tradeoff seems to be that now Solidity would likely have to add a "not 0" check for cases where an address is being provided, but the cost of that would be small compared to the passing in of a sponsee address via calldata.

My thoughts on this:

  • I very much think this is the right thing to do. The whole point of signing over the invoker address is that the sponsee has to trust the logic of the particular invoker contract. Given that this is already a requirement, we might as well make the most out of it and get as much flexibility out of it as possible.

  • One good example for this increased flexibility is the currently hard-coded mingas argument. I think it is not obvious whether it would be preferable to enforce a minimum (as mingas does) or an exact gas amount. For relayer use cases, the user might want a trusted upper limit of what he could have to pay for. If they would have to rely on the relayer logic for that upper limit anyway, why not also rely on it for the lower limit?

  • To me it becomes more and more clear that the nextra argument is much more than a simple replay protection facilitator. I think nextra stands for “nonce extra”, so I would propose a name change either simply to extra or to something more expressive like commit_hash. In addition to the existing chainid, value, mingas, to, data it could also be used for enforcing interesting other restrictions, e.g. for expiring sponsored calls or for a maximum gas price. To me this is one of the main things that make this EIP so exciting…

  • I assume this change would come together with moving most of the arguments from memory to the stack? I initially thought about this opcode from a “sub-transaction” perspective, where I preferred the memory / stack separation. Given that it seems clear that people prefer the “sponsored call” framing, I think this no longer applies and arguments should move to the stack. data obviously has to remain in memory.

  • The one memory / stack decision I am on the fence about is the signature - I think there is a case to be made for v, r, s to remain in memory (somewhat analogous to signatures before they are sent (via calldata) to the ecrecover precompile), instead of them becoming 3 stack elements. They could either be located together with the data, or could have their own memory pointer (no length argument required).

I agree that the ideal framing would be something like valid for the opcode pre-check and success for the success of the actual sponsored call.

New version published: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3074.md

Highlights:

And most importantly:

  • Only sign over type, invoker, chainid, and extra (@adietrichs)

Thanks to everyone who has reviewed so far, and please don’t hesitate to let me know if I’ve missed some of your concerns (or made something else worse!)

1 Like

I have spent today writing a rough first implementation in geth. It is not yet fully compliant with the EIP, I will list the differences & some small questions that came up during implementation tomorrow.

Link to the implementation (of course very much WIP, but I am currently able to successfully execute contracts that use CALLFROM on my little local testnet):

1 Like

New version published (again, sorry)

This time:

  • Removed type from the stack arguments since it’s a constant anyway (@adietrichs)
  • Added a depthLeft argument since exhausting the call stack is a griefing vector (me)
  • Re-added the balance check to the pre-conditions (me)
  • Expanded the invoker security considerations (me)