A case for a simpler alternative to EIP 3074

This has been a very fun read, thank you to @yoavw for so many specific examples and even some alternative approaches.

I’ll be very curious to see if Trezor is prepending a prefix if not. If it’s not, that’s pretty bad. I think one of the strongest take-aways from this thread for me is “we should do a formal and systematic review of wallets’ behavior for this type of signature”

I agree the MetaMask signature warning could be more dire (particularly in a 3074 environment), but it does say in red, that this signature risks entire control of the account. And we wrote that before 3074, so I’m going to count it pretty prescient, even though it could be tighter.

Contract wallets share some of the issues but not all of them. The user makes a deliberate choice to move assets to a contract wallet.

There are now multiple consumer contract wallets that are being promoted to consumers who do not disclaim themselves as having contract bug risk. Vitalik himself promotes contract accounts as a safer alternative to EOAs. We’re already well into the “unspoken account-wide contract risk” era. You could compare that behavior to a wallet signing 3074 messages without mentioning it to the user, for convenience sake. That’s already the wallet environment, so to some degree, I think this argument is trying to prevent a type of risk that is already taken.

That’s a common theme I find when debating delegation: Can we keep people safe, by preventing certain types of delegation? I am of the camp that if a user has the ability to do something, they already have the de-facto ability to delegate it, it might just be inconvenient.

I don’t want to trust their judgement on vetting invokers. I trust them to sign transactions and messages. As for smart contract, I’d rather be vetting them myself.

I think it’s reasonable for a wallet to help users know when they’re taking on contract risk, and I’d gladly make sure that MetaMask lets users avoid this kind of risk entirely, but like with contract accounts, it’s already common for consumer products to integrate contract risk for their users without really emphasizing or mentioning it.

I believe we all agree that EIP 3074 invokers are equivalent to setuid executables. Let’s learn from what went wrong with setuid executables and how the problem was mitigated in the last decades. We don’t need to reinvent security when there’s an equivalent we can learn from. We have decades of relevant security research, and there are time-proven patterns we can study.

I’m not that familiar with the setuid executable example, and I’d love some links to the example attacks and mitigations. From the earlier descriptions in this thread, it sounds like when you’re using chroot, for example, you’re still trusting a trusted computing base to restrict the behavior of subsequent programs being executed. In a way, this sounds a lot like a 3074 invoker in its current form: You trust an invoker, so it can attenuate control to other external scripts.

there doesn’t seem to be any reasonable use case for cross-chain invokers…

One hypothetical could be “I want to delegate this account’s control to this other key, on every network”.

This spectrum between the tradeoffs between user control and safety seems like a very well defined philosophical disagreement. I generally think that the Ethereum platform exists far on the “dangerously free” end of this spectrum, and some of the arguments for “keeping security at the protocol layer” remind me of arguments in favor of per-app blockchains over an application-capable chain like Ethereum. I always figured bugs could exist at the protocol layer, too, so you might as well keep the entire platform more dynamic, but this seems like a distinction that the Ethereum community needs to decide for itself.

Anyways, just sharing my initial thoughts.

I think we should systematically review wallets that sign arbitrary bytes, and for any that do (MetaMask, Trezor?), consider the attack surface (can Ðapps ever propose an arbitrary signature to it? Does it receive a salient warning? How common are users who are willing to paste signature bytes from strangers into outdated signers?)

2 Likes

So this is far from scientific, but I tried your signature, a signature from personal_sign, and a signature with eth_sign on MEW.

Assuming I did all three parts correctly, since MEW validated the trezor signature and the personal_sign signature, but not the raw eth_sign signature, we can assume the trezor signature includes the magic prefix.

Trezor

{
    "address": "0xB46d902CF5B12B8f00c93A6fe3800CDFA4ca4ef7",
    "sig": "0x21ceb8fdd0c8b07c1721a9d1a48365914ca90d1ded9f125208b06a0731b181e30bdb15b10e17ed3c6da2e0f509d2753c0024a10d7787294b3de6523d67949d211b",
    "msg": "\u0003some_hash"
}

Gives

0xB46d902CF5B12B8f00c93A6fe3800CDFA4ca4ef7 did sign the message: some_hash

personal_sign

{
    "address": "0x285608733D47720B40447b1cC0293A2e4435090e",
    "sig": "0x0112651f89e8eaaf0331db857e23f77fe493249cf7d75f0c06ca1ed5e08581c340f0ba8713873cd65bba1d00c6cbdedb235e32130700a3c8f88d8858022eb90c1b",
    "msg": "\u0003some_hash"
}

Gives:

0x285608733D47720B40447b1cC0293A2e4435090e did sign the message: some_hash

eth_sign

{
    "address": "0x285608733D47720B40447b1cC0293A2e4435090e",
    "sig": "0x0667ebe3419e77d411844ab485027580977fa47259ab9780620043bfbaa88a961e188f287ad519fc11d9ac483b79202294ad3bddf2f546474e17ca40d1b43f431b",
    "msg": "\u0003some_hash"
}

Gives:

Signer address is different than the derived address!

To use eth_sign, I had to manually hash the message, which came out to 0x2ff859a3a103a3c50abafb36eaf0ff4a80de20f68766573c10397d1199154515. This is probably the part I’m most unsure of.

1 Like

What kind of glaring trail would you expect to find in the deployment? I don’t think I left anything out. There shouldn’t be any.

To demonstrate it, I quickly hacked together a couple of invoker-like contracts, one that checks commits to verify the call data and prevent replays, and one that doesn’t. I deployed the legitimate one on kovan and the malicious one on ropsten, at the same addresses (0x2f37C1C864932c425Be17E73e9F7d3edc28AF010). The contracts are available with verified code on etherscan so you can inspect their deployment and transactions:

See anything detectable signs of fraud in the deployment of the legitimate invoker on kovan?

And I sent a couple of transactions to demonstrate the attack. I know it’s nowhere near a full simulation of EIP 3074 but the EIP3074Simulation contract implements the security checks that matter for this demo. authTest(bytes32 commit, uint8 _v, bytes32 _r, bytes32 _s) internal returns(address signer) verifies that the commit is signed along with the address of the invoker, returns the signer and assigns it to authorized. function authcallTestAndReset(string memory what) internal returns(bool success) doesn’t actually call anything but it verifies that authorized is set, and performs what on behalf of authorized (actually just emitting SuccessfulExecution(address who, string what)).

Both invokers use that contract without changes, to simulate an EIP 3074 call. But the contracts themselves are different despite having the same address:

BenignInvoker on kovan implements replay protection and ensures that commit really represents what. If you look at the first three transactions I sent it, you can see that the first one (0x324d4bc54319334fb92f4cd5efa93ab37d06e2f95f6fd79395777338a2d8c54d - sorry I can’t link as this forum only allows two links per post) succeeded, and if you look at the decoded input you can see that it executed “legitimate call” on behalf of the signer. The second transaction (0xcf7d3e0154af2acaed3ee9a1b7adfc3d97a0e61b3ca40ea098cb2c1e6568393f) reverted on a replay of the same call, and the third one (0x9869df67fa53fe003265bc09dcc800e2bc08fb75cb0aae189849627404784619) reverted on an attempt to change the what. So this invoker seems legit.

MaliciousInvoker deployed on ropsten omits the checks around commit (no replay protection, no check to associate what with commit). I replayed the commit and signature from the first kovan transaction above, replacing just the what, and sent a couple of transactions malicious whats. If you look at one of them, e.g. 0xb331067354431f085ea300cf748df1416a93430f858402f2595303c74c69bcb0, and decode the input, you’ll see that it is identical to the kovan transaction except for having a malicious what.

The point of this demo is to show the cross-chain attack I was talking about. I think it’s impossible to find anything illegitimate on the kovan invoker, and yet I’m able to perform any action on behalf of the signer on ropsten.

I hope this clarifies the attack and shows why it would be impossible to mitigate without checking chainid in AUTH.

I don’t think it’ll be possible to prove safety of an invoker when it is made so powerful. But the question I keep coming back to, is what functionality do we lose by making it less powerful and have the user actually sign each AUTHCALL? It clearly makes the invoker less risky, so we should compare the two approaches and see what we lose by taking away some of that power.

Well, the initcode is available in the contracts I linked above. I don’t think you can tell that something is wrong by looking at the legitimate one I deployed on kovan.

I used the Linux analogy because Sam described it as setuid (and he’s totally right about it). Since this is the closest analogy, it made sense to draw conclusions from problems it created and how they were mitigated over the years.

I don’t think we ever added something with that level of risk, but perhaps I’m wrong. In any case my philosophy around Ethereum is that we can add new functionality but need to be much more prudent than most other systems. This is not a website, so the Facebook approach of “move fast and break things” doesn’t fit. We should weigh the alternatives and find the least risky way to achieve the goal. I’m not sure EIP 3074 reaches that bar at the moment, but I do think it could with some changes. What I’m trying to do is start a public discussion about the pros and cons of various approaches, so that the community can determine the right trade-offs.

I’m also talking about stateless checks. Specifically, check the same things that you already intend to check on a commit, but do it at the protocol level rather than the invoker. Enforce security in the infrastructure, not an individual contract.

That’s the crux of the matter. The specific attacks we’re discussing can all be mitigated one way or another. The real discussion is about that philosophy. Whether the protocol should be opinionated and enforce the principle if least privilege, or whether we delegate security to a smart contract. My preference is the former, yours seems to be the latter.

That’s fine, we don’t have to share the same philosophy. I highly respect you and the rest of the authors if this EIP, as well as the other commenters in this thread, and I believe we are all working in good faith to make Ethereum better.

The best way to decide is to discuss pros and cons, and let the community decide. It would be helpful if we list some use cases that break if we enforce at the protocol level instead of the invoker, and then we can discuss whether they justify the increased risk.

I haven’t verified, but there’s a good chance it’s actually a message format and not directly signing the transaction (unlike Metamask which does). Still, we’re diving into specifics (e.g. which wallets currently support such messages). Any specific case can be mitigated, including the attacks I demonstrated. The bigger question is whether EIP 3074 really needs to be that powerful, or whether we can achieve much of the same value with a lower risk.

Would it be much more inconvenient to the user if the specific call has to be signed instead of a blank authorization? The wallet could still hide that from the user, but it wouldn’t be as vulnerable to invoker bugs since replay and modifications will be prevented at the protocol level.

It would be quite time consuming to come up with a full list, but if you search “setuid” on old bugtraq archives and CVEs, you can see numerous cases. And then you can see that their frequency starts dropping, as more projects move away from using setuid in favor of daemons that drop privileges. There was a steep decline in these bug reports when Linux completely banned setuid scripts, which broke many projects until they switched to a different model, but greatly improved security. It was controversial at the time, but in retrospect it was the right call. (As an anecdote, you could get root access with such scripts by just setting IFS=/ when invoking them, since it would run “bin” in the current directory as root, instead of /bin/sh, and this worked on almost all Unix based systems for a few years).

I do recall some Usenix Security papers that analyzed the different approaches in the early 2000’s but I haven’t scanned for these old articles. Personally I’ve been involved in this specific space for the past 30 years so it seems obvious to me, but going through privilege escalation CVEs and seeing how many of them were due to setuid might help everyone realize the risk involved.

Unfortunately even today we haven’t fully gotten rid of this problem. The CVE I linked earlier in this thread, from Jan 2021, demonstrates an attack against sudo, one of the last remaining setuid binaries. It’s hard to implement sudo without setuid so it keeps getting hit every couple of years.

As for the mitigation, different approaches were tried, but the prevailing one is daemons that live in a “cage” and only retain the specific capability they need. This is pretty much the standard. In the past few years, since Linux added the unshare(2) syscall, containers like docker started also creating separate namespaces that made things even more secure. We don’t have an equivalent in Ethereum but it’s interesting to think about it. Ethereum strives to be the world computer, and can benefit from past OS security research.

Yes, but the TCB in this case is the kernel which enforces the chroot, the mandatory permissions, the capabilities subsystem, selinux rules, etc. The kernel is verified by more security people than most other parts of the system. The equivalent here would be Ethereum as the TCB. Ethereum itself (and EVM specifically) has been verified and is continuously verified by many security people. Adding 3rd party invokers to the TCB would weaken that model, just like adding a setuid binary weakens the Linux TCB.

This is pretty much the definition of setuid (although setuid was banned for scripts by the Linux kernel due to numerous vulnerabilities, and can now only work with compiled binaries). But since it increases the TCB, it makes sense to consider different approaches that wouldn’t.

But then how would you mitigate the attack I demonstrated above? I don’t think this use-case justifies putting everyone else at such high risk. It would be almost trivial to exploit this.

Why? Per-app blockchains are inherently weaker due to having less participants. Ethereum’s turing-completeness made it attractive enough to become secure. There’s no conflict.

The fact that Ethereum gives us more freedom doesn’t mean that we should become careless. Any EIP needs to be scrutinized and considered against the alternatives. I’d be interested in an analysis of the use-cases that this EIP comes to solve, and whether this is the least risky way to solve them. So far I haven’t seen a use-case that can’t be solved while still letting the user retain the signatory power over the EOA.

As I noted above, I think this is diving into a specific case. I think we should start from a higher level, document the use cases this EIP aims to solve, and then dive down to mitigating specific issues in specific solutions.

I haven’t researched this beyond the snippet I pasted a few messages ago. Considering the above, I suspect that Trezor is not signing a raw message. But since the legacy wallets issue is not the primary issue (just one symptom of possibly giving too much power to the invoker), I think we should focus on the high level first.

Let’s try to get to a design that solves the required use-cases with the lowest risk possible, so that we end up with the most secure version of EIP 3074. When we achieve that, we can go through specific issues (whichever will be left) and see how significant they are and what can be done about them. And finally make a decision on the trade-offs around the ones we can’t mitigate.

Right now I think this thread is mostly debating that last part (trade-offs), treating EIP-3074 as a take-it-or-leave-it proposition and assuming that we just need to decide if the risk is worth it. But I think we should explore improving the design and then maybe we won’t need to debate trade-offs.

1 Like

I retracted my above comment on this, I realize I was only thinking of the create2 case. In the case of create you’re right, there is no way to know and we have to just trust the deployer. This of course is not acceptable.

If we add the requirement that invokers must only be deployed via create2 then that should no longer be an issue. The init code will clearly show if it can sideload code. However, after some more discussion, I think we’re going to put chainid into the auth msg. You presented a compelling argument and although we could tell people to only deploy via create2, we’ve come up with no use cases for multi-chain messages.

Things like chainId and blockNumber can be checked statelessly, because they’re already easily available in the current executing context. There are also no other interpretations of them.

nonce is different. There are many different schemes for replay protection. For example, instead of a sequential nonce you could have a map of tx hash to bool. Enshrining specific nonce mechanisms makes it difficult to have new ones. But the main issue is where do you even store this data? Can’t use EOA nonces because they’re already used by the tx pool to determine tx validity. You could have special precompile with storage, but this would be the first time that’s been done. This is how this EIP actually started, but then we realized the EIP was unlikely to be accepted if it did something weird like that. Finally you could modify the trie to add a new account type / field. Trie changes are very difficult to pull off and making it a prerequisite for the EIP essentially means it won’t be scheduled anytime soon. Plus, none of these changes are as flexible as just allow invokers do arbitrary things.

IMO Ethereum’s philosophy has always been to prefer abstraction over specific implementation when possible.

1 Like

So can to and calldata. There’s little downside in verifying that they’re signed by the EOA. That would remove most attack vectors except for replay.

Right. That’s the only one where there’s a real downside. The most secure option is to make it part of the protocol, e.g. by adding to the trie. But I can see why you want to avoid that for practical reasons. Another option is to use a very simple smart contract that implements the same nonce protection as the EOA one, using its own storage. Technically it doesn’t need to be a precompile, although a precompile with storage has been suggested before. EIP-2935 does that in a way, to save historical block hashes.

While such nonce is the most secure option, it has a clear downside since it is opinionated about the kind of replay protection used, and prevents different protection schemes that would allow better parallelization, more efficient batching, etc.

Another option, less secure but more flexible, is to use multiple storage-based replay protection contracts and whitelist them through EIPs. The AUTH opcode will get an address of a replay protection scheme and a nonce for it. AUTH will verify this tuple along with the rest (to, calldata, chainid, etc.) and then each AUTHCALL would call that.

Each such replay protection contract will be audited by the community and approved through an EIP so they’ll have to meet a higher bar than normal contract. And even if a bug is discovered in one, the implications will be a replay at worst, rather than arbitrary calls on behalf of the EOA.

The least secure option would be the same as above, but without a whitelist. Let each invoker use its own replay-protection contract. It is likely that some of them will be buggy, but still less likely than an invoker having a bug, and with less severe implications since it’s just the replay protection rather than the entire invoker.

I’m inclined to suggest the whitelist-through-EIP approach since it’ll enable the same functionality we get with EIP 3074 with as little security risk as possible.

As far as I can tell, an AUTH that checks the signature on everything (including the nonce and the contract that checks it), and ensures that this contract is in the whitelist, would let us achieve anything that EIP 3074 gives us, with a much lower risk.

Anyone will be able to implement an invoker, while leaving the user in control by signing each call.

And anyone would also be able to come up with a new replay protection scheme, but would have to wait for the next fork to whitelist it.

Do you see any important EIP 3074 use-cases that this scheme can’t support?

You’ve laid out 3 options here:

  1. Max security, put replay protection info in trie.
  2. Medium security, implement replay protection in separate smart contract and allowlist via EIP.
  3. Low security, implement replay protection in separate smart contract and use without allowlist.

Like I mentioned, option 1) is basically a non-starter. EIP-2935 received similar push back.

Option 2) is at odds philosophically with Ethereum. The only time we’ve done something similar to protocol-level allowlisting is with precompiles. This has been a major pain point in core development. Everyone wants their precompile in the next hard fork. Admittedly, precompiles are usually difficult to audit since they are usually implementing specific cryptographic primitives. But regardless, there is significant desire to allow people to write efficient cryptographic primitives without being blocked by hard forks and ACD.

No, this should be able to support all use-cases. Please note, this will inherently be more expensive due the additional contract call and be more complex to handle the edge cases.

Option 3) is really no different functionally than the current proposal of EIP-3074. Yes, the parameters of AUTH change slightly, but invoker implementers can just deploy their replay logic into a separate contract.

I argue that option 2) is equivalent to how we want EIP-3074 to be used in practice. If wallet developers do allowlist certain “safe” EIP-3074 invokers (like they’ve indicated), then the only invokers people will be able to use are the ones that have been “decided” as safe. I don’t see why forcing core devs to debate and decide on this is better than wallet developers doing the same.

If we don’t trust the developers of the wallet we use to upgrade their software securely, I think we have bigger problems.

AFAIK there’s no precedent in Ethereum for anything as powerful as setuid, so it’s hard to draw a philosophy from other precompiles. EIP 3074 would be the equivalent of adding support for additional signature schemes (e.g. BLS), but instead of baking it into the protocol or into precompiles, allow anyone to deploy a contract that implements a new signature type which would work with any EOA for transacting with unaware contracts. I doubt that would be within Ethereum philosophy either.

Yes, it adds the extra cost of an additional CALL. I think it’s worth the significant risk reduction.

As for edge cases, what kind of edge cases would it complicate? I would actually expect it to simplify things because it decouples replay protection from the rest of the invoker logic. The replay protection will have a very simple ABI. It just receives an opaque nonce signed by an EOA (as part of the AUTH message), checks whether this nonce is accepted, and “burn” that nonce. If it fails for whatever reason (fails the nonce check or runs out of gas) the AUTH call reverts. There seems to be little room for edge cases.

From security perspective it is different. EIP-3074 currently gives the invoker a blank check which can be easily abused. option 3 only enables replay. A malicious invoker combined with a malicious replay-verifier could, at worse, replay a transaction previously signed by the EOA for the same chainid. Whereas with the current EIP-3074, the invoker could make any transaction on behalf of any EOA that ever signed a message to it.

I’m less keen on option 3 because it does enable replay attacks by a malicious replay-checker, so I think it should be protected by an “EIP-shield”. But even without the EIP protection, it’s still much more secure than the current proposal.

I think it’s not equivalent. See the explanation above. With option 2, a malicious/buggy invoker can do much less damage than with EIP 3074.

This moves the power from the community (in the form of the public EIP process) to the wallet maintainers. The maintainer of a widely used wallet gets the power to set the standard without going through the scrutiny of the EIP process.

The public process may be inefficient, but it has the advantage of being public. It’s the community’s way to decide for/against something.

The congress with all its debates is also an inefficient way to set policy, but replacing it with an efficient private company seems too risky.

It seems like we fundamentally disagree on the philosophy of Ethereum. It’s unlikely my position on the topic will change, so we can set aside that disagreement for now.

In option 2), if you require AUTH perform a call-like operation into a replay protection contract you’ll need to specify the interface, like you said. This complicates the consensus specification, because right now it does not need to be specified at the consensus-level.

The main edge case I have in mind is what happens if someone calls the replay protection precompile/contract independently of AUTH? How can you securely pass data to the replay protector from AUTH? Do you need to do a second ecrecover or do you bind the replay protection to CALLER or do you magically shuttle it to the precompile? Could this lead to a sponsor being griefed? Should that even be considered by the protocol?

These aren’t unanswerable questions, but it certainly doesn’t simplify things.

From security perspective it is different. EIP-3074 currently gives the invoker a blank check which can be easily abused. option 3 only enables replay. A malicious invoker combined with a malicious replay-verifier could, at worse, replay a transaction previously signed by the EOA for the same chainid. Whereas with the current EIP-3074, the invoker could make any transaction on behalf of any EOA that ever signed a message to it.

As you describe option 3) above, it is no different from EIP-3074 from security perspective. A malicious replay protector can say every nonce is valid. At that point, the malicious invoker again has a “blank check”. There is nothing forcing either the invoker or replay protector to check any part of the commit, where the actions are actually specified. So if a user can be forced to interact with a malicious invoker, they are at equal risk as with EIP-3074 as it’s written.

A malicious invoker under option 2) has the opportunity of 1 blank check, whereas option 3) has N blank checks. After that nonce is burned in option 2), the damage is done.

However, if you can trick a user to signing a tx to a bad invoker, it’s safe to assume you can trick them into signing an arbitrary transaction. I’ve stated on multiple occasions that the argument “but the worst that can happen is one bad tx” is a very weak counter argument to batching. Batching is coming. Whether it’s via meta-txs, ERCs, or EIP-3074. When that time comes, one bad signature will be able to empty an EOA.

Even if you disregard the fact that batching is coming, I don’t think “security by diversification” is a strong argument that 1 blank check is acceptable. I investigated this more a while back.

This is an unfair comparison. You’re framing it like “wallet developers will hold all the power privately and the community will be helpless”. EIP-3074 allows permissionless innovation. If the wallet developers block the adoption of a certain invoker, the community could fork the wallet and add it to the whitelist. Or they could build a new wallet. This is far easier than forking Ethereum to bypass the core developers.

And that’s what I love about this ecosystem :hugs: Disagreeing on philosophy shouldn’t stop us from building an optimal solution together. It actually makes us less likely to fall into the trap of group-thinking.

Right. It means that the interface has to be well defined, just like the args of AUTH itself. I’d make the ABI accept a signed AUTH message because it contains everything this contract needs. It then checks that address(this) is specified as the replay protection contract, extract the EOA from the signature, and use the opaque nonce field in its own logic. Doesn’t seem difficult to define but perhaps I’m missing something.

Why is that a problem? If a user chose to sign an AUTH message and send it directly to the replay protection contract, then the user simply burned a nonce without using it. Just like sending a transaction to self.

One way is to make it a part of AUTH so it will call the contract directly. Another is to change the semantics of AUTH to authorize its caller rather than the current context, so AUTH will only be called inside these whitelisted replay verifiers, and then delegated to the invoker. Neither seem ideal but let’s try to find a better way.

No need for a second ecrecover. The invoker knows that it’s calling a trusted replay protection contract which already does its own ecrecover. If the replay protection call is successful, it means that ecrecover has already succeeded. The call should return the recovered address.

It should be considered, at least to the point of making it possible for sponsors to avoid being griefed. We don’t need to make it impossible to write a bad sponsor that can be griefed though.

Why does it open the sponsor to a griefing attack any more than an EIP 3074 does? It implements exactly the same logic that the invoker would, except that the replay protection becomes an external call. Wouldn’t the same griefing vectors work on both?

You’re right - it doesn’t simplify things. It’s not meant to. It is meant to remove the need for signing blank checks and improve security, by introducing an additional trust boundary. Adding trust boundaries to a system always complicates its internal interactions, but it’s still essential to the security of any complex system.

Consider hardware security mechanisms like SGX (or any HSM for that matter). The programming model of SGX is a pain to work with. It’s much easier to just keep all the secrets in your usermode process memory, perform all operations there, and not have to deal with context switches and stateless ops. But this model makes it possible to secure sensitive operations in a way that protects them from bugs in the rest of the code.

If a system has a small number of sensitive operations, it makes sense to separate them and make them less vulnerable to bugs in the rest of the system. My suggestion is to do just that. If the user signs everything except for the replay protection, then AUTH/AUTHCALL are no longer a major risk. The only remaining risk point is the replay protection, so I’m trying to separate that part.

That’s not what option 3 says. In all three options, I’m only referring to the replay protection, because I assume we already replaced commit with the actual content:

so regardless of the replay protection, AUTHCALL no longer has a blank check. At most, if replay protection is broken, it can AUTHCALL a previously used call. There’s no commit in this model.

I don’t think it is. Let’s assume the user is forced to interact with a malicious invoker. With EIP 3074 it means that the user loses control over the EOA forever. With option 3, there may be two cases:

  1. The user interacts with a malicious invoker which uses a legit replay protector. In that case there is no risk. The invoker can decide whether to perform the AUTHCALL or not, but otherwise has no way to hurt the user.
  2. The user interacts with a malicious invoker which uses a malicious replay protector. This is not possible in option 2 because replay protectors are whitelisted in an EIP. With option 3 it’s equivalent to not having a trusted replay protector, so the user’s transaction may be replayed any number of times in the future. But still, it cannot be modified by the malicious invoker. If the user has 100 DAI and 100 UNI in an account, and signs a transfer of 10 DAI, the malicious invoker can replay it 10 times and steal all 100 DAI, but it can’t touch the UNI. It’s pretty bad, but not nearly as bad as a malicious invoker in EIP 3074. And yet, that’s why I prefer option 2 which also mitigates that.

It’s not what the options say. In all three options, there are no blank checks. AUTH verifies the entire content that used to be covered by commit. The only thing that differs between the three options is how nonce is interpreted.

Batching is coming, but it doesn’t have to mean that a single bad signature will compromise the EOA forever. In the model I suggested above, the user signs a message containing one or more AUTHCALL data. If the user was tricked to misunderstand some of them, then yes, there will be damage. But it doesn’t mean the user loses control of the EOA. With EIP 3074 it does.

Batching, in itself, shouldn’t change the Ethereum security model. The user still needs to authorize every transaction in the batch, just like without batching. The only difference is that they end up being batched for efficiency. If by batching you mean that the user doesn’t need to authorize each transaction, then it completely changes the Ethereum security model.

  1. I do think batching is coming (and should come). It just doesn’t require blank checks.
  2. I didn’t suggest “security by diversification”. Do you see an blank checks in my suggestion?

It does. I never meant that it stops innovation. I’m more concerned about security. The issue is not whether the community can or cannot deploy new invokers (I know it can). It’s whether a malicious wallet maintainer who “plays the long game” (or just has a bug in the invoker) can cause.

A wallet maintainer could devise tricks like the ones I suggested in my first post. The wallet would look honest for months or even years, while collecting signatures. Then, when enough money is on the table, become malicious. For example, the wallet maintainer could use the delegation flow I suggested, allowing its maintainer to take over governance of other projects. The users who just use the wallet for trading, will never notice or complain.

Putting that kind of power in private hands creates incentives for certain people to exploit the system. At some point someone definitely will. By making it public, requiring replay protection to pass the EIP process, it becomes much harder to hide such schemes.

Admittedly, a wallet maintainer could achieve the same goal by stealing and sending private keys to a server and performing the attack from there. However it is far harder to hide that, than to hide a bug in an invoker or not even hide anything at all - just deploy the bad invoker to the other chain after a year (with the current EIP 3074 which excludes chainid).

1 Like

Ah, I see. I apologize, I’m having trouble keeping track of the different posts :slight_smile:

Unfortunately it doesn’t seem like this debate is going anywhere beside nitpicking and trying to keep track of different arguments. I respect your point of view, but I fundamentally disagree with it.

Alright. I respect your point of view as well. Since we’re not collaborating effectively on a modified proposal, I guess EIP 3074 really is a take it or leave it proposition.

We’ll leave it for the community to decide whether to accept it as-is, despite the security issues I pointed out. If it doesn’t get included we can get back to it and propose something with a different risk profile.

I’m not convinced that enshrining single-use transaction-like packages is the way to go

In case you’re worried about the UX of that: consider that wallets could provide per-tx signatures for specific purposes in the background without bothering the user.

1 Like

I’ve been thinking about this thread a bit, and I’ve read up a bit on the types of vulnerabilities that @yoavw has highlighted, and how he’s compared it to setuid.

The thing is, at the heart of the matter I think we agree: Accounts should delegate the minimum possible authority outside of themselves.

The disagreement comes from “at what layer should we allow EOAs to delegate their authority?”

If the only use of delegation were batching and MetaTransactions, then I think yoav’s simpler proposal would be sufficient. But I believe that delegation is fundamental to secure composition, and my interest in 3074 goes far beyond those two use cases.

3074 provides the minimum foundation for a general-purpose delegation framework for EOAs. Yes, it’s dangerous because an initial delegation to a bad invoker can be catastrophic. But, if the invoker is very well audited, and is designed to allow finer-grained additional delegation, we could compose chains of delegations where each link can gain no additional authority. This is a pattern that I’ve been looking out for and have described as ethereum object capabilities.

By providing accounts an initial capability to delegate, we provide a fundamental tool that can be used to enable fine-grained delegations of any sort, and I think this can start to look much more like yoav’s ideal security environment, where additional processes more frequently are truly only getting the capabilities they require.

It may feel counter-productive, or ironic that in order to allow an EOA to delegate the minimum possible authority, they must first be able to delegate any authority, but I think this is basically a result of the EOA’s inability to delegate any authority at the protocol level today. Under the current simplified proposal, any time we want an extra delegation-related feature for accounts, we would need to go through the process of getting it accepted at the base layer of the blockchain. Alternatively, by providing the two very simple opcodes of 3074, we are able to provide any type of delegation in the future on top of the platform, without additional consensus changes. I think Micah said it very well,

1 Like

Thanks, but I can’t take credit for that, although I fully agree with this analogy. I was just quoting @SamWilsn who correctly pointed out that EIP 3074 is setuid and not sudo.

Even if it is perfectly audited, there could be unforeseen risks. Consider the cross-chain attack I demonstrated. The invoker on the first chain was perfectly secure, but once the user moves assets across a bridge to the other chain, there’s a malicious invoker waiting to attack. The user never interacted with the invoker on the other chain. That invoker didn’t even exist at the time the user signed the message. This particular vector can be fixed easily by including chainid in the signed message, but the point I was trying to make is that excluding anything from the signed message opens up vectors we may be missing. There’s little downside in including everything, and a potentially unlimited downside in excluding anything.

I like this chain idea! It’s getting pretty close to capabilities in the Linux sense. The only problem is that each single-capability-invoker is given full access by the user, and we rely on its implementation to restrict the actual action.

My question is why can’t we implement exactly that chain, using the method I suggested. My suggestion is to require that the user (wallet) signs a list of AUTHCALLs rather than a single commit. If the chain involves 5 operations by different invokers, then with EIP 3074 the user would be signing 5 different AUTHs, each of them with full access to another invoker. My suggestion is to sign a specific AUTHCALL for each. So if one of these invokers is buggy, it is still limited to the operation signed by the wallet. We get exactly the same functionality, at the same cost, but with much lower risk.

The UX also remains identical because the wallet would be aware of these 5 invokers and the AUTHCALLs they’re going to make. So a wallet that supports these operations could represent them as a single action for the user to see, and then sign a transaction with these 5 AUTHCALLs rather than 5 AUTHs.

Am I missing something that would break the chained-invokers idea if we use my proposal?

Yes, I think this could be great. We see things similarly. The part that I’m missing is why we need the full access AUTH to achieve this, rather than signing each of these AUTHCALLs.

Right. I think we can achieve the same without having to give up so much control over the EOA. Just sign every operation instead of a single blank check.

You mean my proposal? I wasn’t proposing whitelisting the invokers. Anyone would be able to deploy an invoker and the risk is much lower because an invoker in my proposal can only perform actions signed by the user. Not through a commit to be interpreted by the invoker, but an actual AUTHCALL.

The part that we’ve been debating re whitelist is just the replay protection. If the user has to sign the entire AUTHCALL and the protocol verifies this, then we are stuck with the current nonce implementation which is less friendly to batching. We were debating whether replay protection should be implemented as separate contracts, and those would be whitelisted. In practice I don’t expect to see too many different replay protection schemes, but I do expect to see many different invokers. My proposal doesn’t hinder invoker innovation. It just slows down replay-protection implementations.

And even if we dropped the whitelist entirely, as I suggested in option 3, we still end up with better security than the current EIP. A buggy invoker might lead to a replay, but it would never lead to performing actions the user never signed.

What are the downsides of having the wallet sign things like to and calldata for each AUTHCALL?

1 Like

I think cross-chain is worth considering, but is well accounted for by adding a chain_id parameter to the commitment within the invoker contract. Since it can be such a well documented best practice for an invoker, it seems like it can safely live in either place, but I actually don’t feel very strongly about this. I used the “multiple-chain delegation” as a hypothetical example (it might be nice to be able to delegate cross-chain permissions with a single signature), but it seems like a small enough change I really could go either way on the chain_id point.

My impression is that if this were implemented in this way, these delegations would need to be redeemed in the order they are issued, which tightly couples issuer to redeemer. By allowing an invoker to implement arbitrary redemption logic, we can have “counterfactual”, order-free capabilities being shared, that can be redeemed lazily (resulting in fewer on-chain transactions).

Some delegations might be good for multiple calls.

  • DAIv2 style permit(): It allows issuing a token allowance with a single signature. The redeemer can then redeem/use their allowance in any number of transactions, as long as they don’t exceed that limit.
  • vote-delegation: I might trust another account to vote on a particular topic or a particular DAO on my behalf, any number of times, until revocation.

I demonstrated why it isn’t, with the contracts I deployed and linked earlier in this thread. The commitment is only checked on the first chain. The invoker on the second chain doesn’t even look at the commitment, and just does whatever it wants with the user’s AUTH. The bad invoker on the second chain was deployed after the user already signed the auth on the first chain and was not aware of a future malicious invoker on another chain. Therefore chain_id must be checked by the protocol itself, not by the invoker.

But my chain_id example was just to demonstrate the point, that anything not covered by the protocol-level check may lead to unintended consequences. The current version of the EIP missed that vector. A fixed version that only adds chain_id might still miss other vectors that I haven’t noticed in my preliminary audit. But if we enforce all the fields at the protocol level, there are no cases to miss.

Therefore I think anything should be covered directly by the signature unless there’s a good reason against it.

Why do they need to be ordered? My proposal was to sign a list of call-hashes in AUTH, and then to have AUTHCALL refer to an index in that list. The invoker would be able to trigger them in any order.

But why wouldn’t the wallet arrange them in the expected order? It knows what’s going to happen in the transaction, and could sign a list in the right order.

The user can create an allowance that will last for any number of transactions, without having to repeat it every time. When the allowance runs out, the user needs to create a new allowance. It keeps the user in control. If the user wants to only sign one allowance message and have it last forever, then sign a max allowance. If the user signs a smaller allowance, it means that the user wishes to stay in control of spending, so the invoker should respect that anyway and let the user sign the next allowance. No need to sign an open check for the invoker to auto-renew the allowance.

I think this kind of functionality belongs in smart contracts, not invokers. Most voting contracts explicitly support delegation (e.g. Uniswap style governers). When the user signs such delegation, it’s explicit and the user is in control. By making it easy for invokers to vote directly without the user signing a delegation, we’re opening the network to attacks like the governance-hijacking I described in my first post. This scenario is exactly the one I’m trying to prevent with my proposal.

I think invokers shouldn’t replace smart contracts. They should add functionality that is hard to achieve with smart contracts due to EOA limitations. Vote-delegation and allowance-management don’t seem to fall in that category.

This requires a user to delegate to an invoker that can be published at the same address with arbitrarily different code. That is worth avoiding, but is not unavoidable. Invokers can be published in a way where the code is statically committed to, using CREATE2, and lacking a SELFDESTRUCT op code. Those could be enforced at the wallet level.

I think you’re talking about using the token’s own allowance function. I guess using a token allowance wasn’t an ideal example, since ERC-20 token contracts do provide this kind of delegation for themselves, but they do not provide it in a recursively chainable form.

On the other hand, if an allowance invoker were made, it could allow counterfactual allowances to not only be performed on tokens that never wrote a permit() method, but those allowances could themselves be delegated, allowing recursive allowance graphs, which could form webs of trust with credit, all without publishing a single contract.

The fact that some smart contracts implement delegation for some of their functions is nice, but with a standard delegation framework, contract authors could ignore delegation as a feature, get it for free for all functions, and users could benefit from having those benefits on all contracts. This simplifies secure contract development (since you aren’t asking each developer to re-implement a safe delegation contract).

It would be no different if delegation invokers were well vetted and integrated as an optional user action into wallets. The user would always have explicit control over anything they did. I’m not sure how you’re imagining invokers working, but it seems like you’re imagining some YOLO step that none of us are proposing.

Your initial example relies on a malicious dex performing a long-con where it deploys its own batching invoker, relies on wallets adding it without scrutiny, long-cons many users into adding it to their wallets, and performs malicious behavior that would’ve been obviously possible to any single person who’d reviewed it.

This seems to miss the repeated point that invokers should not be trusted by wallets carelessly. Your scenario requires wallets that are allowing installing invokers with basically no review or warning process for helping users identify dangers, even from an easily-identified malicious contract. Those dangers could be mostly eliminated with just some basic diligent assurances:

  • Most wallets only ever integrate invokers they’ve vetted as if it were their own internal code.
  • Any wallet that allows adding arbitrary invokers needs to at least warn the user that this contract could be stealing all of their funds (and yes, users should learn to respect such warnings). Ideally it would also require a verified contract source code, and would include an audit-warning system, for notifying users of known malicious contracts.

I think in a permissionless ecosystem we should embrace tools that allow us to safely innovate. When I see a tool for generalized delegation, I see enormous potential. Allowances and vote delegation are easy examples to describe, but the actual applications are basically all authority related functions. Anything with an onlyOwner would be made more dynamic with an option to delegate.

I feel a little weird that I’m about to provide more examples in defense of “delegation” as a useful tool, because to me it’s perhaps the most obviously useful tool, but I will list more examples that come to mind:

  • Ability to assign the target of an ENS entry could be shared with an arbitrary group, designated counterfactually. (This could be like sharing the ability to update a website)
  • Ability to freeze a contract (in case of a security threat being discovered)
  • Ability to propose an expense to a DAO (web of trust for proposals)
  • Ability to issue a NFT as part of a collection (artist collectives)
  • Ability to make a move in a game (crowd-sourced gaming)
  • Ability to accept a bounty (web of trust for certified contractors)
  • Ability to buy an exclusive item, like an event ticket (a referral fee could be added as part of the delegation, sometimes called incentive trees)

And yes, delegation can and should live at the smart contract account layer, but that is no reason to ban it from the EOA layer as well. It’s a generally useful tool for composition, and a permissionless ecosystem thrives when composition is cheap and safe.

To stop this particular attack because I already highlighted it. We don’t know how many others exist. The problem is, if an attack vector is discovered and fixed in a wallet at some point in the future, all EOAs that used the wallet and the invoker in the past are still compromised by it.

Suppose we didn’t know about the attack vector I described, and didn’t enforce the CREATE2-only rule, and users already used invokers on different chains. And then I publish about it and all the wallets add this check. Any user who previously used an invoker that was created by CREATE is potentially compromised and needs to move all assets immediately, on all chains. In some cases this won’t even be possible, e.g. with locked tokens.

One of the major issues with EIP 3074 is that it’s impossible to fix things in hindsight. Past vulnerabilities will continue to haunt users, long after they’re fixed. This could have been prevented by checking everything rather than just a commit.

The same could be achieved with the user signing the allowance, since the allowance itself persists so no need to also persist the capability to increase it from an invoker. An invoker (whether based on EIP 3074 or on my proposal) doesn’t need the token to support permit(). The user signs an allowance (once), entrusting the contract to withdraw from the desired token. From then on, things like gas abstraction can rely on that allowance. The invoker can check the cost of the user transaction and withdraw from the allowance, without needing to reestablish the allowance. If the allowance runs out because the user limited it, then the user needs to sign a new one as part of the next transaction. Still no need to give the invoker the power to renew the allowance without the user’s consent. And the allowance could also be delegated by the invoker as well, still without being able to increase it.

With the trade-off of adding a much riskier construct. I’d rather see each contract implement its own delegation, and if the contract is buggy it will have an adverse effect on that contract, than see a single buggy invoker that compromises all contracts at once. Privilege separation limits the potential damage of each bug. The current AUTH removes this separation and puts all the power in a single contract, hoping that it is completely safe. The proverbial putting all the eggs in one basket.

I’m not imagining a YOLO invoker. My concern is a buggy/malicious contract that slips through the audits. Things slip through audits all the time, but their consequences are usually limited. A buggy invoker would compromise everything at once. Any EOA that ever used it, every contract that gives any of these EOAs power (ownership, voting, etc).

So I guess in a way I would consider any transaction to an invoker an act of YOLO. It assumes that the invoker is perfect and trusts it with all assets, now and in the future.

Or with scrutiny that misses an intentional bug. It’s really hard to find intentional bugs and many of them can lurk in trusted code for years despite many audits. Never assume that any code is 100% bug free, especially if there’s an incentive for the developer to introduce a bug.

I do assume a review. I just assume that the reviewers will miss some bugs, as they often do. The consequences of missing an invoker bug are far worse than the consequences in any other contract. A bug in a widely used invoker would make TheDAO look like a minor incident.

All the use cases in the list are possible to do without giving the invoker infinite power, by implementing these capabilities in contracts. When we’re considering adding something so risky, we should only consider use-cases that couldn’t be supported by less risky means. If the same innovation could be supported without the risk, then adding a riskier way to support them seems like a bad trade-off.

What a thread to catch up on. I previously looked into EIP 3074 as it was comparable to what I’m implementing purely in smart contracts. It factors in the concerns being discussed here around invoker implementation/trust, and replay protection via chainid & nonce.
So although not at the protocol level this might be a less-risky implementation of AA, albeit less elegant in some ways.

Here the “invoker” contract (VerificationGateway.sol) is responsible for generating the smart contract wallets given a public key and corresponding signed data (BLS sig scheme).

A user’s BLSwallet contract can make a generic .call with arbitrary data only if called from its invoker (see action). The contract wallet is also responsible for incrementing its nonce.

A user must sign the chainId and the wallet contract’s nonce with the call data (amongst other things). This signed message can then be passed to the invoker to then action the corresponding signer’s BLSWallet.

Anyone (eg aggregator) can submit these signed messages to the invoker contract, and they are incentivised by a token reward. The amount to pay is part of the message signed for and is transferred by the invoker when making the wallet call.

There will also be a demo to construct, sign, and submit a dapp’s smart contract calls (individually or batched), and it might be some time before we see such things implemented in wallet plugins/apps/hardware.

My position on this as well. Though I do not have as much patience as you do to keep arguing :slight_smile: I feel like there is a fundamental change in what it means to give a signature. But arguments usually involve lots of use cases that I need to get into, and I get the feeling that the “burden of proof” is the reverse of what it should be. I also suggested an alternative, which may be similar to yours (multi-signature transaction type), but I do not have time to prepare an EIP properly, so I effectively lose the arguments because of that :frowning: