EIP-3074: AUTH and AUTHCALL opcodes

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.

1 Like

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

1 Like

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

2 Likes

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)

Before talking about my implementation details, there is one last somewhat substantial change that I have been thinking about, namely the separation of the signature verification and the call. My motivation:

  • The opcode doing both seems overloaded, with 13 arguments right now
  • It is non-trivial to implement gas pricing that only charges for the (outer part of the) call portion if the signature is correct
  • In its combined form there is no native support for multiple calls from the same sponsee - they each require an additional signature verification overhead
  • Passing in the sponsee address, while a sensible gas saving, feels like a bit of a hack
  • The opcode returning two stack elements is unusual

My alternative idea would be:

  • AUTHORIZE
    • takes in: extra, v, r, s
    • returns: sponsee or 0
    • validiates the signature
    • sets a new context variable authorizedSponsee
  • AUTHORIZEDCALL
    • takes in: gas, addr, value, in_offset, in_length, ret_offset, ret_length
    • returns: success
    • throws if authorizedSponsee is 0
    • otherwise, behaves exactly like CALL, just with msg.sender set to authorizedSponsee

This has several advantages:

  • Native bundled calls from the same sponsee (one AUTHORIZE, any number of AUTHORIZEDCALL)
  • Cleaner opcodes, AUTHORIZECALL almost identical to CALL
  • Sensible way to recover sponsee address without need to pass it in from the outside
  • It allows for clear separation of invoker logic, where calls are only attempted in the case of a valid signature

Note that extra can be used to commit to a specific list of transactions, so no functionality is lost.

2 Likes

This would effectively be saying, “this caller can do whatever it wants from my account” right? I’m assuming the authorization would only be valid for the call frame where AUTHORIZE was called? Presumably, any user doing this would essentially fully trust the contract it is giving permission to?

From a cleanliness and future capabilities perspective, I really like the idea. However, it means that signing tools will have significantly less information available to present the user with as to what exactly they are signing. From the signing tool’s perspective, the user is just giving blanket power to a particular address.


Note: The signature would need to be over invoker.

1 Like

I think that is a correct summary. Regarding the implications, I think it is important to distinguish between the two main use cases I see for this EIP:

  • In a trusted context (for example for significantly reducing gas usage of centralized exchanges, or for users controlling multiple addresses from a smart contract wallet and/or without the need for ETH in every account), this invoker trust assumption is not an issue, as the sponsor and the sponsee are the same person. I would expect a multitude of implementations, that all only have to pass the usual smart contract security scrutiny.

  • In a trustless context (mainly transaction relaying), trust into the specific invoker implementation is important. However, I think this has already been true for all of the latest revisions of the EIP, once the signed fields became minimal. I would not expect wallets to expose functionality to sign over arbitrary invokers (or if they do, word it in a “delegate full account control to contract” way). Instead / in addtion, there can be a handful of community vetted ERCs for different flavors of relayer systems, with wallets implementing more precise native support for the specific extra hash used by the ERC. So for those ERC invokers, the wallet would not ask for full account control delegation, but instead would display the specific transaction (bundle) to be authorized, with specific information like expiry and other functionality.

Correct. Discussing this with @SamWilsn and @matt we felt like this authorizedSponsee should also not be passed along for delegate calls, to enforce all authorization-related execution to be in one place.

Correct, the signed data would remain identical to the current EIP. No need for the invoker address to be an opcode argument to AUTHORIZE though, because it would be the address of the currently executing contract.

Overview of the proposal with specs explained in PEEPanEIP with @SamWilsn

2 Likes

I think that require(tx.origin != signerAddress) should be removed. Without this limitation, this EIP provides significantly more value because it would allow people to do the “approve and call” flow with ERC20 tokens in a single transaction. In such cases, they are paying for their own gas and they shouldn’t have to go out and find a relayer and pay them extra or fund/use a second account just to get around this limitation.

Also, I don’t see any meaningful value in enshrining the require(tx.origin == signerAddress) behavior because it was already a broken protection that can be bypassed today.

I think the value for the call should be deducted from the signer’s account, rather than the invoker’s account. This would allow users to use this functionality to do transaction batching (i.e., execute the following series of operations in sequence) where one or more of the transactions they want to batch attaches ETH.

If there is fear that such a change would delay the launch of this EIP, then I propose we launch with require(value == 0) and then after a more careful evaluation we can remove that constraint and deduct from the signer.

Hi @jpitts, could we have this thread renamed to “EIP-3074: AUTH and AUTHCALL opcodes” to match the latest EIP title?

From shemnon on GitHub:

I would want to see how this interacts with account abstraction before I would be comfortable moving forward with it. I concur with alexey and martin in that it is too early in the design to freeze it for implementation, which moves it out of London.

Pretty much the whole reason I started investigating this EIP was because EIP-2711 is not compatible with account abstraction. There’s an (admittedly small) section on compatibility in EIP-3074.

In a bit more detail:

With mempool rules, we ban using AUTHCALL before PAYGAS. AUTH should be fine, as far as I can tell. After PAYGAS, an account abstraction contract behaves the same as a regular contract, and would be able to AUTHCALL to its heart’s content.

There are certainly some concerns with creating an ungriefable abstract account sponsor. The AA sponsor wouldn’t be able to rely on external contracts for swapping, or token balances, or pretty much anything, but that’s no different than AA without EIP-3074.

That said, I don’t see anything that would preclude an AA contract that receives ETH or WETH deposits, and then lets you use that to pay for transactions from your EOA.

From Alexey on Discord:

Sponsored Transaction Precompile - not to include to London - is there a way to do it without precompile (these are ugly things and should not be regarded as normal artefacts of EVM development), also consider who exactly will need it and what are the alternatives.

We’ve switched off the precompile (now two opcodes.)

As for interested parties, we have several! @stonecoldpat (who has been active on this thread) is from Infura Transactions/any.sender. I’ve had a bit of contact with OpenZeppelin about this EIP as well, though no strong endorsement (yet!)

The alternatives are:

Natively Set msg.sender

Avoid msg.sender Entirely

I posted a thread on twitter giving a high-level overview of how this EIP will be used in practice: https://twitter.com/lightclients/status/1371911245561917441?s=20

2 Likes

No problem, I’ll update it now…

1 Like

I’ve written a document to discuss some different threat models EIP-3074 should be considered under and how they compare to EOAs and smart contract wallets today: https://hackmd.io/@matt/BknnAnyNu

EIP-191 specifies a safe format for signed messages, which already contains a subformat for messages of the type (ID || validator || payload). Could you use that rather than reinventing the wheel?

Per @timbeiko’s request, here’s a link to a twitter thread I’ve written on this EIP.

Probably my most important comments from the thread are:

Okay, so top question this is going to provoke, especially for “should we commit to shipping this tomorrow?”: IS IT SECURE? I believe the answer is YES, and for a simple reason:
EIP-3074 signatures always have a 0x3 prefix, which currently no Ethereum wallet will sign (at least without a warning that you may be signing everything away). For this simple reason, we can already safely say “no user can become vulnerable to this without accepting the risk.”

Also:
Fun challenge: What other features could you implement for any account with this pattern? While these invokers all derive their power from a signature, they can have state, and are replay-strategy agnostic! You could have a cold wallet that delegates to an m-of-n multisig!

1 Like