Allow externally owned accounts to delegate control to a contract.
|Authors||Sam Wilson (@SamWilsn), Ansgar Dietrichs (@adietrichs), Matt Garnett (@lightclient), Micah Zoltu (@micahzoltu)|
Table of Contents
- Signature in Memory
- Signing Address auth Argument
- Reserving One Sixty-Fourth of Available Gas
- Throwing for Unset authorized During AUTHCALL
- Another Sponsored Transaction EIP
- What to Sign?
- Understanding commit
- Invoker Contracts
- On Call Depth
- Source of value
- Allowing tx.origin as Signer
- AUTHCALL cheaper than CALL when sending value
- Backwards Compatibility
- Security Considerations
This EIP introduces two EVM instructions
AUTHCALL. The first sets a context variable
authorized based on an ECDSA signature. The second sends a call as the
authorized account. This essentially delegates control of the externally owned account (EOA) to a smart contract.
Adding more functionality to EOAs has been a long-standing feature request. The requests have spanned from implementing batching capabilities, allowing for gas sponsoring, expirations, scripting, and beyond. These changes often mean increased complexity and rigidity of the protocol. In some cases, it also means increased attack surfaces.
This EIP takes a different approach. Instead of enshrining these capabilities in the protocol as transaction validity requirements, it allows users to delegate control of their EOA to a contract. This gives developers a flexible framework for developing novel transaction schemes for EOAs. A motivating use case of this EIP is that it allows any EOA to act like a smart contract wallet without deploying a contract.
Although this EIP provides great benefit to individual users, the leading motivation for this EIP is “sponsored transactions”. This is where the fee for a transaction is provided by a different account than the one that originates the call.
With the extraordinary growth of tokens on Ethereum, it has become common for EOAs to hold valuable assets without holding any ether at all. Today, these assets must be converted to ether before they can be used to pay gas fees. However, without ether to pay for the conversion, it’s impossible to convert them. Sponsored transactions break the circular dependency.
top - N- the
Nth most recently pushed value on the EVM stack, where
top - 0is the most recent.
||- byte concatenation operator.
- invalid execution - execution that is invalid and must exit the current execution frame immediately, consuming all remaining gas (in the same way as a stack underflow or invalid jump).
MAGIC is used for EIP-3074 signatures to prevent signature collisions with other signing formats.
The context variable
authorized shall indicate the active account for
AUTHCALL instructions in the current frame of execution. If set,
authorized shall only contain an account which has given the contract authorization to act on its behalf. An unset value shall indicate that no such account is set and that there is not yet an active account for
AUTHCALL instructions in the current frame of execution.
The variable has the same scope as the program counter –
authorized persists throughout a single frame of execution of the contract, but is not passed through any calls (including
DELEGATECALL). If the same contract is being executed in separate execution frames (ex. a
CALL to self), both frames shall have independent values for
authorized. Initially in each frame of execution,
authorized is always unset, even if a previous execution frame for the same contract has a value.
A new opcode
AUTH shall be created at
0xf6. It shall take three stack element inputs (the last two describing a memory range), and it shall return one stack element.
The final two stack arguments (
length) describe a range of memory. The format of the contents of that range is:
memory[offset : offset+1 ]-
memory[offset+32 : offset+33]-
memory[offset+64 : offset+65]-
memory[offset+96 : offset+97]-
Memory is not modified by this instruction.
length is greater than 97, the extra bytes are ignored for signature verification (they still incur a gas cost as defined later). Bytes outside the range (in the event
length is less than 97) are treated as if they had been zeroes.
authority is the address of the account which generated the signature.
The arguments (
s) are interpreted as an ECDSA signature on the secp256k1 curve over the message
keccak256(MAGIC || chainId || paddedInvokerAddress || commit), where:
chainIdis the current chain’s EIP-155 unique identifier padded to 32 bytes.
paddedInvokerAddressis the address of the contract executing
AUTH(or the active state address in the context of
DELEGATECALL), left-padded with zeroes to a total of 32 bytes (ex.
commit, one of the arguments passed into
AUTH, is a 32-byte value that can be used to commit to specific additional validity conditions in the invoker’s pre-processing logic (e.g. a nonce for replay protection).
Signature validity and signer recovery is handled analogously to transaction signatures, including the stricter
s range for preventing ECDSA malleability. Note that
yParity is expected to be
If the signature is valid and the signer address is equal to
authority, the context variable
authorized is set to the
authority. In particular, this is also true if
authority == tx.origin, which used to be handled separately in earlier versions of this EIP (see Security Considerations). If the signature is instead invalid or the signer address does not equal
authorized is reset to an unset value.
authorized is set, or
The gas cost for
AUTH is equal to the sum of:
- fixed fee
- memory expansion gas cost (
The fixed fee is equal to the cost for the
ecrecover precompile, plus a bit extra to cover a keccak256 hash and some additional logic.
The memory expansion gas cost (
auth_memory_expansion_fee) shall be calculated in the same way as
RETURN, where memory is expanded if the specified range is outside the current allocation.
A new opcode
AUTHCALL shall be created at
0xf7. It shall take eight stack elements and return one stack element. It matches the behavior of the existing
0xF1) instruction, except where noted below.
AUTHCALL is interpreted the same as
CALL, except for (note: this list is also the order of precedence for the logical checks):
authorizedis unset, execution is invalid (as defined above). Otherwise, the caller address for the call is set to
- The gas cost, including how much gas is available for the subcall, is specified in the Gas Cost section.
- If the
gasoperand is equal to
0, the instruction will send all available gas as per EIP-150.
- If the gas available for the subcall would be less than
gas, execution is invalid.
- There is no gas stipend, even for non-zero
valueis deducted from the balance of the executing contract. It is not paid by
valueis higher than the balance of the executing contract, execution is invalid.
valueExtis not zero, the instruction immediately returns 0. In this case the gas that would have been passed into the call is refunded, but not the gas consumed by the
AUTHCALLopcode itself. In the future, this restriction may be relaxed to externally transfer value out of the
AUTHCALL must increase the call depth by one.
AUTHCALL must not increase the call depth by two as it would if it first called into the authorized account and then into the target.
The return data area accessed with
0x3e) must be set in the same way as the
AUTHCALL does not reset
authorized, but leaves it unchanged.
The gas cost for
AUTHCALL shall be the sum of:
- static gas cost (
- memory expansion gas cost (
- dynamic gas cost (
- gas available for execution in the subcall (
The memory expansion gas cost (
memory_expansion_fee) shall be calculated in the same way as
The dynamic gas portion (
dynamic_gas), and the gas available for execution in the subcall (
subcall_gas) shall be calculated as:
dynamic_gas = 0 if addr not in accessed_addresses: dynamic_gas += 2500 # cold_account_access - warm_storage_read if value > 0: dynamic_gas += 6700 # NB: Not 9000, like in `CALL` if is_empty(addr): dynamic_gas += 25000 remaining_gas = available_gas - dynamic_gas all_but_one_64th = remaining_gas - (remaining_gas // 64) if gas == 0: subcall_gas = all_but_one_64th elif all_but_one_64th < gas: raise # Execution is invalid. else: subcall_gas = gas
CALL, the full gas cost is charged immediately, independently of actually executing the call.
The signature format (
s) is fixed, so it might seem curious that
auth accepts a dynamic memory range. The signature is placed in memory so that
auth can be upgraded in the future to work with contract accounts (which might use non-ECDSA signatures) and not just EOAs.
authority (the signing address) as an argument to
auth allows future upgrades to the instruction to work with contract accounts, and not just EOAs.
authority were not included and multiple signature schemes allowed, it would not be possible to compute the authorizing account’s address from just the signature alone.
AUTHCALL will not pass more than 63/64th of the available gas for the reasons enumerated in EIP-150.
A well-behaved contract should never reach an
AUTHCALL without having successfully set
authorized beforehand. The safest behavior, therefore, is to exit the current frame of execution immediately. This is especially important in the context of transaction sponsoring / relaying, which is expected to be one of the main use cases for this EIP. In a sponsored transaction, the inability to distinguish between a sponsee-attributable fault (like a failing sub-call) and a sponsor-attributable fault (like a failing
AUTH) is especially dangerous and should be prevented because it charges unfair fees to the sponsee.
There are two general approaches to separating the “fee payer” from the “action originator”.
The first is introducing a new transaction type. This requires significant changes to clients to support and is generally less upgradeable than other solutions (e.g. this EIP). This approach is also not immediately compatible with account abstraction (AA). These proposals require a signed transaction from the sponsor’s account, which is not possible from an AA contract, because it has no private key to sign with. The main advantage of new transaction types is that the validity requirements are enforced by the protocol, therefore invalid transactions do not pollute block space.
The other main approach is to introduce a new mechanism in the EVM to masquerade as other accounts. This EIP introduces
AUTHCALL to make calls as EOAs. There are many different permutations of this mechanism. An alternative mechanism would be add an opcode that can make arbitrary calls based on a similar address creation scheme as
CREATE2. Although this mechanism would not benefit users today, it would immediately allow for those accounts to send and receive ether – making it feel like a more first-class primitive.
Besides better compatibility with AA, introducing a new mechanism into the EVM is a much less intrusive change than a new transaction type. This approach requires no changes in existing wallets, and little change in other tooling.
AUTHCALL’s single deviation from
CALL is to set
CALLER. It implements the minimal functionality to enable sender abstraction for sponsored transactions. This single mindedness makes
AUTHCALL significantly more composable with existing Ethereum features.
More logic can be implemented around the
AUTHCALL instruction, giving more control to invokers and sponsors without sacrificing security or user experience for sponsees.
As originally written, this proposal specified a precompile with storage to track nonces. Since a precompile with storage is unprecedented, a revision moved replay protection into the invoker contract, necessitating a certain level of user trust in the invoker. Expanding on this idea of trusted invokers, the other signed fields were eventually eliminated, one by one, until only
invoker binds a particular signed message to a single invoker. If invoker was not part of the message, any invoker could reuse the signature to completely compromise the EOA. This allows users to trust that their message will be validated as they expect, particularly the values committed to in
Earlier iterations of this EIP included mechanisms for replay protection, and also signed over value, gas, and other arguments to
AUTHCALL. After further investigation, we revised this EIP to its current state: explicitly delegate these responsibilities to the invoker contract.
A user will specifically interact with an invoker they trust. Because they trust this contract to execute faithfully, they will “commit” to certain properties of a call they would like to make by computing a hash of the call values. They can be certain that the invoker will only allow they call to proceed if it is able to verify the values committed to (e.g. a nonce to protect against replay attacks). This certainty arises from the
commit value that is signed over by the user. This is the hash of values which the invoker will validate. A safe invoker should accept the values from the user and compute the commit hash itself. This ensures that invoker operated on the same input that user authorized.
commit as a hash of values allows for invokers to implement arbitrary constraints. For example, they could allow accounts to have
N parallel nonces. Or, they could allow a user to commit to multiple calls with a single signature. This would allow mult-tx flows, such as ERC-20
transfer actions, to be condensed into a single transaction with a single signature verification. A commitment to multiple calls would look something like the diagram below.
The invoker contract is a trustless intermediary between the sponsor and sponsee. A sponsee signs over
invoker to require they transaction to be processed only by a contract they trust. This allows them to interact with sponsors without needing to trust them.
Choosing an invoker is similar to choosing a smart contract wallet implementation. It’s important to choose one that has been thoroughly reviewed, tested, and accepted by the community as secure. We expect a few invoker designs to be utilized by most major transaction relay providers, with a few outliers that offer more novel mechanisms.
An important note is that invoker contracts MUST NOT be upgradeable. If an invoker can be redeployed to the same address with different code, it would be possible to redeploy the invoker with code that does not properly verify
commit and any account that signed a message over that invoker would be compromised. Although this sounds scary, it is no different than using a smart contract wallet via
DELEGATECALL. If the wallet is redeployed with different logic, all wallets using its code could be compromised.
The EVM limits the maximum number of nested calls, and naively allowing a sponsor to manipulate the call depth before reaching the invoker would introduce a griefing attack against the sponsee. That said, with the 63/64th gas rule, and the cost of
AUTHCALL, the stack is effectively limited to a much smaller depth than the hard maximum by the
It is, therefore, sufficient for the invoker to guarantee a minimum amount of gas, because there is no way to reach the hard maximum call depth with any reasonable (i.e. less than billions) amount of gas.
value passed into an
AUTHCALL is deducted from the invoker’s balance. A natural alternative source for
value would be the
authorized account. However, deducting value from an EOA mid-execution is problematic, as it breaks important invariants for handling pending transactions. Specifically:
- Transaction pools expect transactions for a given EOA to only turn invalid when other transactions from the same EOA are included into a block, increasing its nonce and (possibly) decreasing its balance. Deducting
authorizedaccount would make transaction invalidation an unpredictable side effect of any smart contract execution.
- Similarly, miners rely on the ability to statically pick a set of valid transactions from their transaction pool to include into a new block. Deducting
authorizedaccount would break this ability, increasing the overhead and thus the time for block creation.
At the same time, the ability to directly take ether out of the
authorized account is an important piece of functionality and thus a desired future addition via an additional opcode similar to
AUTHCALL. For this reason, it is included as
valueExt, an operand of
AUTHCALL, which may be activated in a future fork. The prerequisite for that would be to find satisfying mitigations to the transaction invalidation concerns outlined above. One potential avenue for that could be the addition of account access lists similar to EIP-2930, used to signal accounts whose balance can be reduced as a side effect of the transaction (without on their own constituting authorization to do so).
authorized to equal
tx.origin enables simple transaction batching, where the sender of the outer transaction would be the signing account. The ERC-20 approve-then-transfer pattern, which currently requires two separate transactions, could be completed in a single transaction with this proposal.
AUTH allows for signatures to be signed by
tx.origin. For any such signatures, subsequent
msg.sender == tx.origin in their first layer of execution. Without EIP-3074, this situation can only ever arise in the topmost execution layer of a transaction. This EIP breaks that invariant and so affects smart contracts containing
require(msg.sender == tx.origin) checks. This check can be used for at least three purposes:
- Ensuring that
msg.senderis an EOA (given that
tx.originalways has to be an EOA). This invariant does not depend on the execution layer depth and, therefore, is not affected.
- Protecting against atomic sandwich attacks like flash loans, that rely on the ability to modify state before and after the execution of the target contract as part of the same atomic transaction. This protection would be broken by this EIP. However, relying on
tx.originin this way is considered bad practice, and can already be circumvented by miners conditionally including transactions in a block.
- Preventing re-entrancy.
Examples of (1) and (2) can be found in contracts deployed on Ethereum mainnet, with (1) being more common (and unaffected by this proposal.) On the other hand, use case (3) is more severely affected by this proposal, but the authors of this EIP did not find any examples of this form of re-entrancy protection, though the search was non-exhaustive.
This distribution of occurrences—many (1), some (2), and no (3)—is exactly what the authors of this EIP expect, because:
- Determining if
msg.senderis an EOA without
tx.originis difficult (if not impossible.)
- The only execution context which is safe from atomic sandwich attacks is the topmost context, and
tx.origin == msg.senderis the only way to detect that context.
- In contrast, there are many direct and flexible ways of preventing re-entrancy (ex. using a storage variable.) Since
msg.sender == tx.originis only true in the topmost context, it would make an obscure tool for preventing re-entrancy, rather than other more common approaches.
There are other approaches to mitigate this restriction which do not break the invariant:
tx.originto a constant
tx.originto the invoker address for
tx.originto a special address derived from any of the sender, invoker, and/or signer addresses.
authorized == tx.origin. This would make the simple batching use cases impossible, but could be relaxed in the future.
Sending non-zero value with
CALL increases its cost by 9,000. Of that, 6,700 covers the increased overhead of the balance transfer and 2,300 is used as a stipend into the subcall to seed its gas counter.
AUTHCALL does not provide a stipend and thus only charges the base 6,700.
Although this EIP poses no issues for backwards compatibility, there are concerns that it limits future changes to accounts by further enshrining ECDSA signatures. For example, it might be desirable to erradicate the concept of EOAs altogether, and replace them with smart contract wallets that emulate the same behavior. This is fully compatible with the EIP as written, however, it gets tricky if users can then elect to “upgrade” their smart contract wallets to use other methods of authentication – e.g. convert into a multisig. Without any changes,
AUTH would not respect this new logic and continue allowing the old private key to perform actions on behalf of the account.
A solution to this would be at the same time that EOAs are removed, to modify the logic of
AUTH to actually call into the account with some standard message and allow the account to determine if the signature / witness is valid. Further research should be done to understand how invokers would need to change in this situation and how best to write them in a future-compatible manner.
The following is a non-exhaustive list of checks/pitfalls/conditions that invokers should be wary of:
- 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.
valueshould be included in
commit. Without it, a malicious sponsor could cause unexpected effects in the callee.
gasshould be included in
commit. Without it, a malicious sponsor could cause the callee to run out of gas and fail, griefing the sponsee.
calldatashould be included in
commit. Without them, a malicious actor may call arbitrary functions in arbitrary contracts.
A poorly implemented invoker can allow a malicious actor to take near complete control over a signer’s EOA.
authorized to equal
tx.origin has the possibility to:
- Break atomic sandwich protections which rely on
- Break re-entrancy guards of the style
require(tx.origin == msg.sender).
The authors of this EIP believe the risks of allowing
authorized to equal
tx.origin are acceptable for the reasons outlined in the Rationale section.
Copyright and related rights waived via CC0.
Please cite this document as:
Sam Wilson (@SamWilsn), Ansgar Dietrichs (@adietrichs), Matt Garnett (@lightclient), Micah Zoltu (@micahzoltu), "EIP-3074: AUTH and AUTHCALL opcodes [DRAFT]," Ethereum Improvement Proposals, no. 3074, October 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3074.