EIP-3074: AUTH and AUTHCALL opcodes

Thanks for sharing a concrete example.

Thanks in part to CREATE2, I think you’re right that an invoker could be changed at the same address. There are simple ways to completely avoid this, like only trusting invokers that are published with CREATE, since their address generation cannot be reproduced.

This is a good example of a serious consideration that needs to be taken when delegating to an invoker, but I don’t think this is new. Wallets are effectively already constantly exposed to this kind of attack (new code could silently undermine user trust at a later date), it’s just that it’s client-side, not always on-chain (although contract-based wallets are basically already vulnerable to the exact scenario you describe). I don’t think this changes the threat model of wallet development in any way.

Fortunately, no one is exposed to these risks by 3074 without someone at a later date making a deliberate choice to integrate the invoker into a wallet, or to sign a delegation message, and even then, the risk is limited to the negligent parties (or that wallet’s users).

Fortunately, this conversation would not end at the inclusion of EIP-3074, instead it would represent the beginning of the design of secure invokers. I have no doubt that good invokers can be built that are widely vetted and easy to trust, and will leave this particular concern comfortably to rest.

1 Like

I’m involved in the development of the Minerva Wallet and was pretty happy when reading about this EIP.
3074 reminds me of the Proxy pattern which separates state from logic.
In a way, it would provide all EOAs with the optionality of behaving like a proxy contract which can freely choose its implementation contract with every transaction. To me that sounds appealing both from a wallet provider and from a user perspective.

3074 vs contract wallets

I agree that in theory everybody should just use contract wallets.
In practice however they have their own set of tradeoffs:

  • they seem to still not be first class citizens in large parts of the ecosystem.
    There’s (still) a lot of tooling and contracts out there assuming the sender to be an EOA. I don’t have any stats supporting that claim, just my subjective impression.
  • they can pose an entry barrier.
    Creating an EOA is a trivial off-chain activity which can take place on any device, doesn’t require connectivity and doesn’t require any funds or sponsor. Creating an account with a contract wallet requires either a funded EOA to start from or a sponsor.
    I guess however that disadvantage was at least partially mitigated with CREATE2.
  • contract wallets create a kind of vendor lock-in.
    As a user, I was always hesitant to commit to a contract wallet. That’s because I know that migrating from a wallet using a contract account (often a contract specific to that wallet) to another wallet is a non-trivial operation.

Intuitively I’d say that 3074 kind of retrofits EOAs with optional contract wallet functionality.

Security

I’m probably not qualified enough to comment about the overall tradeoffs in terms of complexity/attack surface/security etc. on a protocol level. Just wanted to comment on the specific concern voiced by @akolotov about potential signature harvesting.
I hardly see a significant difference btw. a conventional EOA and an 3074 enabled EOA there. If I were a blackhat able to inject code into a wallet today which allowed me to sign arbitrary messages, I’d just harvest a lot of signatures, varying nonces, token addresses (most popular first), amounts (not knowing what the balance will be when I’m ready to steal). That may not allow me to steal and break everything, but would likely come pretty close in many cases.
That’s assuming that I couldn’t just steal the PK itself anyway (as was the case e.g. in the 2018 Copay incident).

2 Likes

Follow the development update, Puxi testnet progress & a demo with other useful discussions from the EIP-3074 community call #2

5 Likes

I’m sorry if these questions were asked already, but:

  1. Why isn’t chainid part of the signed data for AUTH? i.e. keccak256(MAGIC || paddedChainId, paddedInvokerAddress || commit). yParity seems to be restricted to 0/1, so it is not utilising EIP-155-style chainid inclusion.

  2. Is there a strong reason to keep retOffset/retLength in AUTHCALL? If the only reason is “being similar to other CALL*s”, then I would suggest to not have. If the reason is that contract developers would prefer using that as opposed to returndatacopy, then of course it makes a lot of sense to keep it.

  3. Why is AUTH/AUTHCALL separated? Is it motivated by a) simplicity; b) allowing multiple AUTHCALLs; c) having the ability to check if a signature is valid without executing a call?

I believe the answer is mostly b) (and c) comes as a benefit), but it could be much better explained.

1 Like

If the authorized contract wants to check chain ID, it can. It is not up to the EVM to do all possible validations, and anywhere that validation can be delegated to the contract it is.

Is retOffset/retLength just a legacy holdover from before returndatacopy was introduced? If so, then I think we should remove them.

There are a number of use cases where the signature may not sign over the calldata. A few examples:

  1. one auth, multiple calls (approve and transfer)
  2. permanent auth (one signature that gives contract 100% perpetual control)
  3. one auth with multiple code paths that lead to different calls
1 Like

We have come to be very excited about this EIP over at statechannels.org as it allows for a novel contract architecture we might call synthetic-EOA-per-channel. We believe this is likely to be the most elegant and efficient architecture for state channels, offering a large reduction in blockchain state usage as well as an improved user experience.

The EIP authors introduced us to the idea of a “synthetic, externally-owned” account. It is somewhat like a contract account in that the associated private key is not known or used, but very different to a contract account in that it cannot have any code or storage. The address of the account is found by recovering the public key from a fixed message digest. For example, using an EIP-3074 digest:

digest = keccak256(MAGIC || padTo32Bytes(Invoker) || padTo32Bytes(0))

and a “synthetic signature”, constructed without the use of a private key*. Using the EIP-3074 digest hands complete control of the synthetic EOA to the Invoker, as explained in the EIP.

State channels may then be executed by participants sending funds directly to a synthetic EOA of their choice — one derived from the channelId, for example (which is roughly speaking the hash of the participants’ accounts). The Invoker can then release funds from the synthetic EOA conditional on all of the usual checks defined by the state channel protocol (e.g. the channelId itself, signatures on off-chain states, etc).

This avoids the need to i) have an additional “layer 2 accounting system” for state channels (i.e. no need to use contract storage to record which channel has how many tokens because the ethereum accounting system suffices), or ii) deploy a contract per channel using CREATE2 (which is expensive and will shortly become more so as SELFDESTRUCT refunds are removed) and also avoids the need to iii) approve and transferFrom tokens into the state channel in two separate transactions.

The bottom line from a state channels point of view is: improved state hygiene by avoiding writes to storage and/or code deposits and improved UX. Both imply increased gas efficiency. Projects like Connext are executing a high volume of state channels in production, making these improvements all the more significant.

*We would like a deterministic, reliable algorithm for this: it seems somewhat nontrivial and worthy of some more research.

4 Likes

I wanted to bubble up an interesting idea from Jeff Coleman on Twitter:

To flesh out the idea just a bit more, we could write an invoker that takes the normal transaction inputs, but replace to: Address with to: ENS. This way, the signature of the AUTH msg would be over MAGIC || INVOKER || hash([nonce, to_ens, gas_limit, gas_price, value, data]).

Doing this binds the msg to the to_ens handle and allows the hardware wallet to actually show to: someperson.eth rather than 0x12..34. The invoker can then trustlessly resolve the Ethereum address associated with the ENS domain during execution and route the rest of the call to that address.

Resolving the ENS name on-chain is the only way to trustlessly interact with ENS. Even if you run your own node, there is nothing binding standard transactions to a certain ENS domain. If you secure your assets with the hardware wallet (instead of with a smart contract wallet), you won’t be able to interact with ENS address trustlessly. You’ll always need to verify the Ethereum address on the device against what the ENS name should resolve to in order to be certain no part of the stack was compromised.

1 Like

Dropping a link to a follow-up post:

Am I right, that making one wrong signature would allow attacker to drain whole wallet: all tokens and ETH from the EOA balance?

EIP-3074 cannot move ETH from EOA balances, but technically yes. However that’s only half the story. To even get to that point, your wallet would have to allow you to create a bad signature. We trust our wallets to not do this every time we send a transaction, why think about EIP-3074 differently?

@matt but you can’t spend all your ERC20 balances within 1 transaction now.

Not today, but tx batching will come. EIP-3074 will expedite it, but it’s coming regardless, and at that point the risk profiles will be the same.

A possibility to ease security concerns is to add an AUTHRELEASE opcode. This opcode could be called by any contract that had previously received an AUTH from an EOA and would relinquish the authorization (for a particular commit of course).

This allows for contracts to implement two new kinds of functionalities. First, they could create a batchExecuteThenRelease function, which would allow users to send a signed AUTH message for batch execution, but could call AUTHRELEASE at the end of the execution to ensure that the contract does not retain indefinite control of the EOA. The second feature that can be built with this is a revoke function for an invoker, which would simply call AUTHRELEASE for a particular commit, allowing a user to safely remove authorization from a contract that they no longer trust.

Of course, this system has many downsides. To start, these abilities can be created entirely at the smart contract level, so it does not necessarily require a new opcode, but form often fits function, and it is likely if we added this opcode that many invokers would chose to take advantage of it. An additional downside is it would require the precompile to access storage to store who has released an authorization, which I understand has never been done before.

This sort of opcode would significantly increase the cost of AUTH, since there would need to be a state read to check if the EOA has been released or not.

Your proposal also doesn’t outline how the “release” function works. If you release the EOA itself, how do you “un-release”? If you release the signature, you’ll have to store the hash of the signature in the trie which costs ~22k gas.

I was thinking that release would work exactly as you described, where you release the signature, which should be unique. This would mean that calling auth would only have the increased gas cost of reading state. I do agree though that the increased gas costs may make my proposed solution not worth it, especially since these features can be implemented at the smart contract level.

an address is EOA,and someone uses EIP-3074 for this address

can others create this address as a contract?

for example,hash collisions occur on binance chain and ethereum

https://etherscan.io/address/0x491604c0fdf08347dd1fa4ee062a822a5dd06b5d

This likely isn’t hash collision. Just someone sent the address some BNB and CTSI on BSC.

Hey, I thought I’d post this Twitter conversation that’s being had about ERC-3074 and composability, starting from this tweet:

https://twitter.com/dmihal/status/1414579442073194498

The concern is that if application developers begin using 3074 in their contracts, then those contracts basically become EOA-only.

You can take a look at this example from @fubuloubu:

transferFrom(msg.sender, this, amt) Can just be transfer(this, amt, auth=sig)

If applications use transfer{ auth: ... }() instead of transferFrom(), then contracts can no longer use this function, since contracts can’t sign messages (@gakonst pointed out that ERP-1271 could be used, but I think it would be difficult to get much adoption of that).

It seems that these issues can be avoided if application-level use of EIP-3074 is discouraged, and AUTH is only invoked from a handful of whitelisted “invoker” contracts.

https://twitter.com/lightclients/status/1414581676890419200

So my takeaways from this conversation is that usage of EIP-3074 should be limited at these levels:

  1. Wallet layer: wallets should only allow signing 3074 messages for whitelisted invokers (and there should be a very scary warning before adding new invokers to the whitelist)
  2. Language layer: Solidity & Vyper should not add support for 3074 opcodes, invokers should be built in Yul (or using inline Yul)
  3. Social layer: discourage developers from using 3074 opcodes directly, instead encourage them to build for existing invokers.
1 Like

I’ve been under a rock for the past few months, but I was thrilled to learn about 3074 while watching the presentation at EthCC 4. It’s refreshing to finally see more discussion with a focus on improving transaction efficiency and delegation, so that mainstream adoption (especially by enterprise) can happen in the near future. Bravo!

I’m the author of EIP-2746, and so I definitely have an interest in this subject, especially in regard to batching and rules. (I’ll admit that my EIP is definitely not as thought out as this one. I’m absolutely an amateur in this space.) I apologize for not yet being familiar with the implementation, but I was especially curious about the rules being encoded in these AUTHCALL requests (particularly in terms of their flexibility) and about the assumption that these new opcodes could be invoked within a L2.

Basically, I’m wondering what considerations have been made in terms of rule complexity and whether these rules can be other contract methods. I could see an interest in the Invoker executing a standard, lengthy set of rules that are too complicated and too costly to be encoded as part of a transaction’s calldata. In one example, a company might have a complicated validation algorithm that exists as a contract method and simply returns a boolean. In another example, a government or accounting firm could post a contract method (or a EIP-2746 ruletree - shameless plug) that is the standard for calculating a VAT tax, one that could be used by any enterprise without fear, and returns a numeric value. So, my question is this: could an AUTHCALL request result in the Invoker getting input from a contract method (using only a CALL, to be safe), to either evaluate in a rule or to pass along to the sponsored transaction? If not, can the Invoker receive and then store these encoded rules (which are part of the calldata?) on behalf of the caller, so that they can be invoked again at a later time and without the cost of sending them once again? That storage might be costly, but in the case of 3074 being deployed to a L2 (like Optimism), these storage costs could possibly be mitigated. Assuming, of course, that storage might be cheaper on a L2 in the long run.

Very curious about what’s possible. And great work so far!

Whoops…I forgot one question: do you think that EIP-3074 could possibly be compatible with a batching implementation found in a L2, like the Optimism sequencer?