EIP-3074: AUTH and AUTHCALL opcodes

Actually I noticed that this section says that the only signed fields are invoker and commit, contrary to the actual spec. Probably forgot to update this part?

To be clear, I am also in favor of only signing invoker and commit.

Thanks! How’s this: Update eip-3074.md: explain `nonce` and `chainId` in rationale by SamWilsn · Pull Request #8428 · ethereum/EIPs · GitHub

Did I get it right that now it will be possible:

With auth opcodes, imagine you just send eth to some address, but that address is a contract and your wallet won’t even tell you, you think it’s a regular token transfer, but not, the dest will ask your tx for this auth call. You won’t notice anything, but account will be compromised

No, you have to sign a separate message (with the 3074 type byte) and relay it on-chain. Additionally, signing a 3074 auth doesn’t mean your account is compromised. If the invoker is well implemented, it’s just as safe as a transaction from a smart contract wallet.

Hey guys! excited to see this EIP being included in the upcoming fork.

I do have one questions around impact of this EIP that i saw someone discussing and wanna confirm here: Will this EIP be able to enable “permanent” delegation to a invoker contract? I saw someone mentioned an uescase where users can use their EOA to “delegate once” to a multisig and use that to send daily txs on behalf of the EOA.

My understanding is that AUTHCALL can only happen after AUTH within the single call frame, so every time a contract needs to make AUTHCALL, it needs a (new) signature by the original EOA owner. This would mean the usecase above is impossible. Am i correct or is there other ways to bypass this?

when an invoker use an authcall opcode, does the nonce must be the same as auth included? Or the nonce is the value which is from tx.original

Determining if msg.sender is an EOA without tx.origin is difficult (if not impossible.)

Can’t you also check the codesize at that address?

Replay protection (ex. a nonce) should be implemented by the invoker, and included in commit. Without it, a malicious actor can reuse a signature, repeating its effects.

But the invoker contract can not see an EOA’s nonce. The invoker can’t determine whether the authority has the correct nonce at the time of AUTHCALL.

Therefore, I am assuming that the invoker contract maintains its own contract-specific nonce tracking. However, I am not sure how the invoker contract can figure out what the nonce is that was provided in the signed message.

But then it mentions that commit is actually what is used to check these things. For example, commit might be something like keccak256(nonce || "dataAboutSpecificCallToInvoke()"). Therefore, what would the nonce field in the signed message be used for? It seems that this field is optional anyway.

Thank you

I have looked into this EIP and implemented a POC for vyper language support in POC feat[lang]: add EIP-3074 support by charles-cooper · Pull Request #3958 · vyperlang/vyper · GitHub. One thing that bothers me about the EIP is it seems actually like incomplete support for AA. If you think about all the ways to create call context in the EVM, there are at least 5 in regular usage - CALL, STATICCALL, DELEGATECALL, CREATE, CREATE2. However, this EIP only provides a single “authorized counterpart” opcode, AUTHCALL being the counterpart to CALL. This means that you cannot change msg.sender for the other four. While these maybe don’t seem like “super common” use cases, they also are valid use cases! (Otherwise, call contexts created by those other four opcodes would not allow access to msg.sender).

The current proposed spec is also not sympatico with proposals like EIP-7069, which propose upgrades to the existing *CALL opcodes (but there is no analogous EXTAUTHCALL opcode, which would require extra work to spec out, implement and test).

I think an improvement to the spec would be as follows:

  • Remove AUTHCALL opcode entirely(!)
  • AUTH sets an “authorization context” for all following instructions which create call context, not just CALL. For instance, STATICCALL following an AUTH invocation will use the msg.sender specified by the current authorized value. If you want to go back to the “regular” behavior, invoke AUTH again with an invalid signature to clear the current authorization.
  • The following details to make this pattern economic:
    • Update the gas schedule so that re-running AUTH for an (address, signature) pair which has already been called in this calling context is very cheap (<100 gas).
    • Ending the authorization scope can be done by revocation - providing an invalid (address, signature) pair. (This is already enabled in the current spec.)
    • Revocation is very cheap (<15 gas)
    • By convention, revocation can be done by providing the zero address and a zero-length buffer.
      • Or, revocation can be done by convention by providing ADDRESS. I don’t think it matters much.
    • Update the gas schedule to be very cheap for revocations (address does not match signature)
    • Update the gas schedule to respect warm/cold access for the authorized parameter.

I think this pattern is also just much more intuitive for developers. It is similar to dapptools/foundry/titanoboa prank intrinsic, which overrides msg.sender for all calls/creates within the prank scope (not just the specific CALL opcode).

2 Likes

I have a question about the choice of the signature format of this EIP.

In particular, it is defined to expect a signature with recovery information (yParity). Then the authentication process of the opcode essentially boils down to:

success := authority == ecrecover(msg, r, s, yParity)

However, based on my understanding of ECDSA, it would be possible to just use signature verification instead of recovery here and implement AUTH as:

success := ecverify(authority, msg, r, s)

Allowing you to completely drop the yParity from the signature. While this doesn’t save much (although, to my understanding EC verification is slightly less computation), it does make the opcode function with a nice round 96 byte signature (3 x 32-byte words).


Edit: Oh, silly me, only the address is known and not the public key. Oops!

Permanent-until-revoked, yes. As long as you don’t send a regular transaction from the EOA, authorizations remain valid forever.

Using AUTH does not burn the nonce. You can re-use the same authorization as many times as the invoker allows, until the EOA’s nonce is incremented.

1 Like

No, because contracts have zero codesize while running the initcode (aka constructor.)

1 Like

I already replied in a chat, but repeating here for visibility:

I was reviewing our open PR for AUTH/AUTHCALL in Kakarot and had exactly the same question: why AUTHCALL while AUTH + checking in system operation opcodes if the message is authorized would be enough?

I wouldn’t classify this as very unsafe. Like other opcode making state changes, you need to keep track of it when you consider the safety of a contract.

To me, as soon as a contrat consumes an AUTH somewhere, it means in any case that it is understood that somehow everything can be done on behalf of the signing EOA.

Changing the behaviour of *call functions makes this really weird:

function foo() public {
    this.someErc20.approve(...)
}

function sudo(...) public {
    assembly {
        auth(...)
    }
    this.foo();
}

you mean

function foo() public {
    this.someErc20.approve(...)
}

function sudo(...) public {
    assembly {
        auth(...)
    }
    this.foo();
}

? (changed authcall to auth)

I kinda like this tbh, what’s wrong with this snippet?

Oops, edited. Thanks!

As an auditor, you now have to trace the entire possible callstack to ensure that someErc20.approve is never called while authorized.

I’d say the same as @charles-cooper

ie “much more intuitive for developers, similar to existing tools”

The argument in favor of AUTHCALL on the other hand is also that there already 4 CALLs, almost the same with some variations for who is sender, so why not a dedicated 5th CALL for when using the authorized sender

1 Like

I like to follow both fail-safe design and the principle of least astonishment. It’s objectively safer to explicitly request changing msg.sender, and much more surprising if some nested subcall gets msg.sender changed.