From acc1c12353780ad3e4bd3ee843ca842c876d3b07 Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Mon, 11 May 2020 22:40:25 +0100 Subject: [PATCH 01/11] EIP proposal draft --- docs/EIP-proposal.md | 186 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 docs/EIP-proposal.md diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md new file mode 100644 index 0000000..b63e178 --- /dev/null +++ b/docs/EIP-proposal.md @@ -0,0 +1,186 @@ +--- +eip: +title: CallWithSigner - A new CALL opcode to replace msg.sender with the signer's address +author: +discussions-to: +status: draft +type: Standards Track +category: Core +created: 2020-05-11 +--- + +## Simple Summary + +Ethereum transaction's intertwine the identity of who paid for the transaction (tx.gaspayer) and who wants to execute a command (msg.sender). As a result, it is not straight forward for Alice to pay the gas fee on behalf of Bob who wants to execute a command in a smart contract. If this issue can be fixed, then it allows Bob, without any significant hurdles, to outsource his transaction infrastructure to Alice in a non-custodial manner. This EIP aims to alleviate the issue by introducing a new opcode, `callWithSigner()`, in the EVM. + +## Abstract + +It is common practice to authenticate the immediate caller of a contract using msg.sender. An immediate caller can be the externally owned account that signed the Ethereum Transaction or another smart contract. We propose a third option, `callWithSigner()`, that checks if an externally owned account has authorised the command (alongside relevant replay protection) before calling the target contract. If the checks pass, then the target contract is invokved and msg.sender is assigned as the externally owned account. + +## Motivation + +The idea of a [meta-transaction](https://medium.com/@austin_48503/ethereum-meta-transactions-90ccf0859e84) was first popularized by Austin Griffth as a mechanism to onboard new users by paying for their gas. It allows an externally owned account (Bob) to sign a command and for another party (Alice) to take on the responsibility of publishing it to the network (alongside paying the gas fee). What makes the mechanism truly useful is that Bob can outsource his entire transaction infrastructure in a non-custodial manner to Alice. She takes on the role of a [non-custodial proxy-bidder](https://ethresear.ch/t/first-and-second-price-auctions-and-improved-transaction-fee-markets/2410/5) in the network's fee market. We are starting to witness the uptake of meta-transactions with third party wallet providers, dapps that pay gas for their users, and relayer APIs. + +There are two existing solutions that try to solve the problem, but they have the following shortcomings: + +**Proxy contract**. This approach requires users to migrate their funds into a proxy contract. While the additional work-flow of deploying a proxy contract and transferring funds is inconvenient for adoption, the real hurdle is the additional security risks associated with storing funds in a smart contract and whether users will trust the solution due to significant events like the Parity Wallet hack. As well, a subtle problem is how to migrate way/recover from the situation when the provider of the proxy contract disappears (e.g. the wallet manages the proxy contract and discontinues its service, but no other service is using the same approach). + +**Upgrade target contract**. This approach requires the target contract to natively support meta-transactions and it does not work for pre-existing smart contracts. The `Permit()` function is intrusive as it requires the target contract to natively handle replay protection (e.g. verify the user's signature and then increment nonce by 1). `msgSender()` tries to alleviate the contract intrusiveness as the replay protection is handled by a global singleton RelayHub contract and the target contract is only required to replace msg.sender with msgSender() . So far no single approach has achieved wide-spread adoption and this is most evident in [Gnosis Safe](https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol#L193) that implements three solutions to the msg.sender problem. This includes 1) checking a signed message from the externally owned account that is not compatible with Permit(), 2) checking the message hash for uniquness and 3) checking contract signatures via EIP-1271. + +The fragmentation of solutions to the msg.sender problem is evident that it is indeed a real problem faced by contract developers. They have tried to solve it at the contract-level, but it is fundamentally a platform issue. This motivates us to propose a new opcode `callWithSigner()` that can be implemented in the EVM. As a bonus point, the opcode `tx.origin` will finally have a meaningful purpose and it should be renamed `tx.gaspayer`. + +## Specification + +We propose `callWithSigner()` that checks: + +- The externally owned account has signed and authorised the call (e.g. target contract and its calldata) +- The signed replay protection is unique (e.g. this is the first and only time the command is executed) + +If both checks pass, then the target contract is invoked with the desired calldata and the signer's address is set as msg.sender. Of course, this proposal requires the new opcode to maintain storage as it must keep track of the replay protection used so far by all signers. + +For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly increment by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. + +For the interface, we propose: + +``` +targetContract.callWithSigner(callData, replayProtection, signature, signer) +``` + +It has the following parameters: +`targetContract`: Address of the target contract. Same as CALL. +`calldata`: Encoded function name and data. Same as CALL. +`replayProtection`: Encoded replay protection of two nonces (queue and queueNonce) +`signature`: Authorised command signed by the user +`signer`: Externally owned account address + +We assume the following encoding / signing: + +`replayProtection` -> `abi.encode(["uint","uint"],[queue,queueNonce]);` +`signature` -> `Sign(keccak256(targetContract, callData, replayProtection, chainid))` + +The additional `chainid` is to verify the signature is for the target blockchain (mainnet/ropsten/etc). + +To specify how to implement callWithSigner we provide an example in pseudo-Solidity for ease of reading. + +We assume there is a global mapping of replay protection: + +``` +mapping(bytes32 => uint256) public nonceStore; +``` + +The opcode needs to check the replay protection is valid: + +``` +// Check the signer's replay protection is valid +function verifyReplayProtection(address _signer, bytes memory _replayProtection) internal returns(bool) { + uint queue; uint queueNonce; + (queue, queueNonce) = abi.decode(_replayProtection, (uint256, uint256)); + // Notice the signer's address and queue computes the index + bytes32 queue = keccak256(abi.encode(_signer, _queue)); + uint256 storedNonce = nonceStore[queue]; + + if(queueNonce == storedNonce) { + nonceStore[index] = storedNonce + 1; + return true; + } + + return false; +} +``` + +The opcode needs to verify the signer's signature: + +``` +function verifySig(address _targetContract, bytes memory _callData, bytes memory _replayProtection, bytes memory _signature) public view returns (address) { + + bytes memory encodedData = abi.encode(_targetContract, _callData, _replayProtection, this.chainid); + + return ECDSA.recover(ECDSA.toEthSignedMessageHash(keccak256(encodedData)), _signature); +} +``` + +Altogether the final functionality: + +``` +function callWithSigner(address _targetContract, bytes memory _callData, bytes memory _replayProtection, bytes memory _signature, address _signer) public { + + require(verifyReplayProtection(_replayProtection, _signer), "Replay protection is not valid"); + require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); + + msg.sender = signer; // Override msg.sender to be signer + _targetContract.call(_callData); +} +``` + +As we can see in the above, the opcode simply checks replay protection and the signer's signature before overriding msg.sender and then executing a normal .call(). + +## Rationale + +The rationale to favor a new opcode is the following: + +- **Non-intrusive change**. It does not impact existing tooling or wallets. They can simply ignore the new opcode unless it is required. +- **Minimal functionality**. The only job of the new opcode is to check the user has signed the message and that it has not been replayed. Thus it is emulating the existing Ethereum Account system, but at the EVM level. +- **Application logic surrounding the opcode**. Relayers like any.sender and GSN implement logic before forwarding the call (e.g. to record a log the job was done or to reward the relayer). By making it an opcode and not as an Ethereum Transaction, it is easy to wrap additional logic around it. + +There are two alternative approaches that we describe below. + +**Pre-signed Ethereum Transaction.** Instead of the interface/data structure proposed in this EIP, another approach is to supply a pre-signed Ethereum Transaction to the new opcode. The advantage is that we can re-use the existing account system for the replay protection, re-use significant portions of code (both in the node and client-side) to verify/generate the transaction. However, it likely has a larger data-structure overhead (e.g. RLP decoding, additional fields, etc), it mixes the replay protection of both systems (which may not be desirable) and it limits the signer to NONCE replay protection (single queue). We mention the approach as it is a desirable alternative that should be considered. + +**Modify Ethereum Transaction**. An alternative approach for solving the problem is to modify the structure of an Ethereum Transaction to include a new field for the signer's address, signature/replayprotection and the command. The EVM can simply check if the fields are filled in are correct before swapping msg.sender with the signer's address. Of course, if the fields are omitted, then msg.sender == tx.origin. However modifying the structure of an Ethereum Transaction is an intrusive and significant change. It may require all wallets and tooling to upgrade to support the new EIP. Thus the rationale for the new opcode is the following: + +We provide some brief information in regards to related work: + +[Account abstraction](https://docs.ethhub.io/ethereum-roadmap/ethereum-2.0/account-abstraction/). This work aims to remove the the distinction of externally owned accounts and contract accounts. While the original account abstraction proposal is a drastic change and may not be implemented in ETH1, our proposal for callWithSigner is way to implement something similar to account abstraction. The opcode can be wrapped in contract logic while keeping the signer's address as msg.sender. As such all transactions are sent via a contract wallet, the contract logic is procesed, and then the signer's address is kept when calling the target contract. **We highlight our goal is to work with the existing account system, but it may enable a subset of account abstraction.** + +[Rich transaction precompile](https://github.com/Arachnid/EIPs/blob/f6a2640f48026fc06b485dc6eaf04074a7927aef/EIPS/EIP-draft-rich-transactions.md). This work lets a signer execute a batch of calls in a single Ethereum Transaction while maintaining msg.sender as the signer of the transaction. It is desirable to streamline the user experience (e.g. one transaction to perform several actions). Our proposal for callWithSigner can achieve a similar effect as the contract code that surrounds the the opcode can be used to send a batch of transactions. For example: + +``` +for(uint i=0; i + +Replay protection testcases: + +- For queue=0, the first nonce=0 is accepted. first nonce=0 for queue=0 is accepted. +- For queue=0, the nonce is accepted if it is incremented sequentially. +- For queue=0, the nonce is rejected if it has skipped a number (e.g. nonce=3 instead of nonce=1). +- For queues [0,...,50], the first nonce=0 is accepted. +- For queues [0,...,50], the nonce is incremented sequentially for each queue and it is accepted. +- For queue=0 and queue=3, the first nonce=0 is accepted. +- Replay protection is rejected due to bad encoding (e.g. 3 uints instead of 2) + +Signature testcases: + +- Signature is valid if the replay protection and target contract/calldata is valid. +- It will not verify the user's signature if the replay protection is invalid/used already. + +Call testcases. In all cases, if the transaction passes, then it should test that msg.sender is the signer's address: + +- Transaction should succeed if the target contract and calldata is executed. +- Transaction should succeed if calldata requires more than 1 argument. +- Transaction should revert if the target contract and calldata do not match. + +More tests can and should be added. The above is a small sample for the initial draft of the EIP. + +## Implementation + +We do not yet have an implementation of the new opcode/precompile. But we provided an example in pseudo-Solidity for the specification. This should provide clarity on how it can be implemented if this EIP moves to that stage. + +## Security Considerations + +Our proposal is potentially the first opcode/precompile that requires persistent storage which may come with its own security/reliability challenges. Furthermore if the replay protection or signature verification is not implemented correctly, then it can facilitate impersonation and/or replay attacks. The final implementation code should be small and the project is well-scoped, so it should be reasonable to audit. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 03e5a51d9ba0a54955fe58f78e6d3938c99aa10d Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Mon, 11 May 2020 22:42:38 +0100 Subject: [PATCH 02/11] making code a bit more readable --- docs/EIP-proposal.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index b63e178..73a0bcd 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -73,8 +73,10 @@ The opcode needs to check the replay protection is valid: ``` // Check the signer's replay protection is valid function verifyReplayProtection(address _signer, bytes memory _replayProtection) internal returns(bool) { + uint queue; uint queueNonce; (queue, queueNonce) = abi.decode(_replayProtection, (uint256, uint256)); + // Notice the signer's address and queue computes the index bytes32 queue = keccak256(abi.encode(_signer, _queue)); uint256 storedNonce = nonceStore[queue]; @@ -105,6 +107,7 @@ Altogether the final functionality: function callWithSigner(address _targetContract, bytes memory _callData, bytes memory _replayProtection, bytes memory _signature, address _signer) public { require(verifyReplayProtection(_replayProtection, _signer), "Replay protection is not valid"); + require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); msg.sender = signer; // Override msg.sender to be signer From d2af4fc5d5e0aebb5b2f4efb6d18c30ab62e9ad0 Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Mon, 11 May 2020 22:45:26 +0100 Subject: [PATCH 03/11] fixed bullet points for the parameters --- docs/EIP-proposal.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index 73a0bcd..35188c1 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -46,17 +46,18 @@ For the interface, we propose: targetContract.callWithSigner(callData, replayProtection, signature, signer) ``` -It has the following parameters: -`targetContract`: Address of the target contract. Same as CALL. -`calldata`: Encoded function name and data. Same as CALL. -`replayProtection`: Encoded replay protection of two nonces (queue and queueNonce) -`signature`: Authorised command signed by the user -`signer`: Externally owned account address +It has the following parameters: + +- `targetContract`: Address of the target contract. Same as CALL. +- `calldata`: Encoded function name and data. Same as CALL. +- `replayProtection`: Encoded replay protection of two nonces (queue and queueNonce) +- `signature`: Authorised command signed by the user +- `signer`: Externally owned account address We assume the following encoding / signing: -`replayProtection` -> `abi.encode(["uint","uint"],[queue,queueNonce]);` -`signature` -> `Sign(keccak256(targetContract, callData, replayProtection, chainid))` +- `replayProtection` -> `abi.encode(["uint","uint"],[queue,queueNonce]);` +- `signature` -> `Sign(keccak256(targetContract, callData, replayProtection, chainid))` The additional `chainid` is to verify the signature is for the target blockchain (mainnet/ropsten/etc). From 77c1c8fc349d16386086fa857e3b3539e1a369f8 Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Tue, 12 May 2020 14:04:08 +0100 Subject: [PATCH 04/11] updated based on comments --- docs/EIP-proposal.md | 65 ++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index 35188c1..4304e1c 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -15,7 +15,7 @@ Ethereum transaction's intertwine the identity of who paid for the transaction ( ## Abstract -It is common practice to authenticate the immediate caller of a contract using msg.sender. An immediate caller can be the externally owned account that signed the Ethereum Transaction or another smart contract. We propose a third option, `callWithSigner()`, that checks if an externally owned account has authorised the command (alongside relevant replay protection) before calling the target contract. If the checks pass, then the target contract is invokved and msg.sender is assigned as the externally owned account. +It is common practice to authenticate the immediate caller of a contract using msg.sender. An immediate caller can be the externally owned account that signed the Ethereum Transaction or another smart contract. We propose a third option, `callWithSigner()`, that checks if an externally owned account has authorised the command (alongside relevant replay protection) before calling the target contract. If the checks pass, then the target contract is invoked and externally owned account is assigned as msg.sender. ## Motivation @@ -23,23 +23,37 @@ The idea of a [meta-transaction](https://medium.com/@austin_48503/ethereum-meta- There are two existing solutions that try to solve the problem, but they have the following shortcomings: -**Proxy contract**. This approach requires users to migrate their funds into a proxy contract. While the additional work-flow of deploying a proxy contract and transferring funds is inconvenient for adoption, the real hurdle is the additional security risks associated with storing funds in a smart contract and whether users will trust the solution due to significant events like the Parity Wallet hack. As well, a subtle problem is how to migrate way/recover from the situation when the provider of the proxy contract disappears (e.g. the wallet manages the proxy contract and discontinues its service, but no other service is using the same approach). +**Proxy contract**. Each user has their own proxy contract. They must migrate funds into the proxy contract and all transactions are sent via the proxy contract. As such, the msg.sender is set as the proxy contract address. There are three hurdles for proxy contracts: -**Upgrade target contract**. This approach requires the target contract to natively support meta-transactions and it does not work for pre-existing smart contracts. The `Permit()` function is intrusive as it requires the target contract to natively handle replay protection (e.g. verify the user's signature and then increment nonce by 1). `msgSender()` tries to alleviate the contract intrusiveness as the replay protection is handled by a global singleton RelayHub contract and the target contract is only required to replace msg.sender with msgSender() . So far no single approach has achieved wide-spread adoption and this is most evident in [Gnosis Safe](https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol#L193) that implements three solutions to the msg.sender problem. This includes 1) checking a signed message from the externally owned account that is not compatible with Permit(), 2) checking the message hash for uniquness and 3) checking contract signatures via EIP-1271. +1. **Workflow issues.** There is an additional and inconvenient work-flow of deploying a proxy contract and transferring funds. This can hinder adoption as it is not a straight-forward plug & play experience. +2. **Two addresses.** The user now has two addresses which includes the signing address and the proxy contract address. This needs to be managed as part of the user experience and some dapps may need to take it into account. +3. **Trust issues.** Users may have trust issues with storing funds in a smart contract due to the additional security risks. Several events including the Parity Wallet Hack exacerbate the problem. -The fragmentation of solutions to the msg.sender problem is evident that it is indeed a real problem faced by contract developers. They have tried to solve it at the contract-level, but it is fundamentally a platform issue. This motivates us to propose a new opcode `callWithSigner()` that can be implemented in the EVM. As a bonus point, the opcode `tx.origin` will finally have a meaningful purpose and it should be renamed `tx.gaspayer`. +Finally there is a subtle problem on how to recover (and migrate away) from the proxy contract if the provider disappears. For example, if the wallet managing access to the proxy contract discontinues its service, but no other service is using the same standard. + +**Upgrade target contract**. This approach requires the target contract to natively support meta-transactions and it does not work for pre-existing smart contracts. There are two popular approaches: + +1. The `Permit()` function is intrusive as it requires the target contract to natively handle replay protection (e.g. verify the user's signature and then increment nonce by 1). + +2. `msgSender()` tries to alleviate the contract intrusiveness as a global singleton RelayHub contract is responsible for handling the replay protection. The target contract is only required to replace msg.sender with msgSender() . + +So far no single approach has achieved wide-spread adoption and this is most evident in [Gnosis Safe](https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol#L193) that implements three solutions to the msg.sender problem. This includes checking a signed message from the externally owned account that is not compatible with Permit(), checking the message hash for uniqueness (i.e. a form of replay protection) and finally checking contract signatures via EIP-1271. + +The fragmentation of solutions to the msg.sender problem is evident that it is indeed a real problem faced by contract developers. They have tried to solve it at the contract-level, but it is fundamentally a platform issue. This motivates us to propose a new opcode `callWithSigner()` that can be implemented in the EVM. As a bonus point, the opcode `tx.origin` will finally have [a meaningful purpose](https://github.com/ethereum/solidity/issues/683) and it should be renamed `tx.gaspayer`. ## Specification We propose `callWithSigner()` that checks: - The externally owned account has signed and authorised the call (e.g. target contract and its calldata) -- The signed replay protection is unique (e.g. this is the first and only time the command is executed) +- The replay protection is unique (e.g. this is the first and only time the command is executed) -If both checks pass, then the target contract is invoked with the desired calldata and the signer's address is set as msg.sender. Of course, this proposal requires the new opcode to maintain storage as it must keep track of the replay protection used so far by all signers. +If both checks pass, then the target contract is invoked with the desired calldata and the signer's address is set as msg.sender. Of course, the new opcode requires long-term storage to keep track of the latest replay protection used (e.g. it is achieved with a single mapping that links the signer's address to the latest nonce). For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly increment by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. +We provide the rest of this specification in pseudo-Solidity for ease of reading (and its motivation originates [from here](https://github.com/anydotcrypto/metatransactions/blob/master/src/contracts/account/RelayHub.sol). + For the interface, we propose: ``` @@ -54,15 +68,13 @@ It has the following parameters: - `signature`: Authorised command signed by the user - `signer`: Externally owned account address -We assume the following encoding / signing: +We assume the following for the encoding and the signing: - `replayProtection` -> `abi.encode(["uint","uint"],[queue,queueNonce]);` - `signature` -> `Sign(keccak256(targetContract, callData, replayProtection, chainid))` The additional `chainid` is to verify the signature is for the target blockchain (mainnet/ropsten/etc). -To specify how to implement callWithSigner we provide an example in pseudo-Solidity for ease of reading. - We assume there is a global mapping of replay protection: ``` @@ -98,7 +110,7 @@ function verifySig(address _targetContract, bytes memory _callData, bytes memory bytes memory encodedData = abi.encode(_targetContract, _callData, _replayProtection, this.chainid); - return ECDSA.recover(ECDSA.toEthSignedMessageHash(keccak256(encodedData)), _signature); + return ECDSA.recover(keccak256(encodedData), _signature); } ``` @@ -116,7 +128,7 @@ function callWithSigner(address _targetContract, bytes memory _callData, bytes m } ``` -As we can see in the above, the opcode simply checks replay protection and the signer's signature before overriding msg.sender and then executing a normal .call(). +As we can see in the above, the opcode checks the replay protection and the signer's signature before overriding msg.sender and then executing a normal .call(). ## Rationale @@ -128,15 +140,15 @@ The rationale to favor a new opcode is the following: There are two alternative approaches that we describe below. -**Pre-signed Ethereum Transaction.** Instead of the interface/data structure proposed in this EIP, another approach is to supply a pre-signed Ethereum Transaction to the new opcode. The advantage is that we can re-use the existing account system for the replay protection, re-use significant portions of code (both in the node and client-side) to verify/generate the transaction. However, it likely has a larger data-structure overhead (e.g. RLP decoding, additional fields, etc), it mixes the replay protection of both systems (which may not be desirable) and it limits the signer to NONCE replay protection (single queue). We mention the approach as it is a desirable alternative that should be considered. +**Pre-signed Ethereum Transaction.** It is possible to supply a pre-signed Ethereum Transaction to the new opcode. We can re-use the existing account system for the replay protection and re-use significant portions of code (both in the node and client-side) to handling the transaction. However, it does involve a more complicated data-structure (e.g. RLP decoding, additional fields, etc) and it may not be desirable to mix the replay protection of both systems. As well, it limits the signer to NONCE replay protection (single queue). We mention the approach as it is a desirable alternative that should be considered and it has been implemented in the [GSN](https://github.com/opengsn/gsn/blob/master/contracts/Penalizer.sol#L28). -**Modify Ethereum Transaction**. An alternative approach for solving the problem is to modify the structure of an Ethereum Transaction to include a new field for the signer's address, signature/replayprotection and the command. The EVM can simply check if the fields are filled in are correct before swapping msg.sender with the signer's address. Of course, if the fields are omitted, then msg.sender == tx.origin. However modifying the structure of an Ethereum Transaction is an intrusive and significant change. It may require all wallets and tooling to upgrade to support the new EIP. Thus the rationale for the new opcode is the following: +**Modify Ethereum Transaction**. We can modify the structure of an Ethereum Transaction to include a new field for the signer's address, signature & replay protection and the calldata. The EVM can check if the fields are filled in are correct before swapping msg.sender with the signer's address. Of course, if the fields are omitted, then msg.sender == tx.origin. However modifying the structure of an Ethereum Transaction is an intrusive and significant change. It may require all wallets and tooling to upgrade to support the new EIP. We provide some brief information in regards to related work: -[Account abstraction](https://docs.ethhub.io/ethereum-roadmap/ethereum-2.0/account-abstraction/). This work aims to remove the the distinction of externally owned accounts and contract accounts. While the original account abstraction proposal is a drastic change and may not be implemented in ETH1, our proposal for callWithSigner is way to implement something similar to account abstraction. The opcode can be wrapped in contract logic while keeping the signer's address as msg.sender. As such all transactions are sent via a contract wallet, the contract logic is procesed, and then the signer's address is kept when calling the target contract. **We highlight our goal is to work with the existing account system, but it may enable a subset of account abstraction.** +[Account abstraction](https://docs.ethhub.io/ethereum-roadmap/ethereum-2.0/account-abstraction/). It removes the distinction of externally owned accounts and contract accounts. In a way, it is similar to the proxy contract approach where the user's funds are stored in the contract wallet and that is the default msg.sender on the network. As a result, this EIP may not be required as there is no such thing as an 'externally owned account' and thus the signer's address is never used as msg.sender. -[Rich transaction precompile](https://github.com/Arachnid/EIPs/blob/f6a2640f48026fc06b485dc6eaf04074a7927aef/EIPS/EIP-draft-rich-transactions.md). This work lets a signer execute a batch of calls in a single Ethereum Transaction while maintaining msg.sender as the signer of the transaction. It is desirable to streamline the user experience (e.g. one transaction to perform several actions). Our proposal for callWithSigner can achieve a similar effect as the contract code that surrounds the the opcode can be used to send a batch of transactions. For example: +[(EIP not assigned) Rich transaction precompile](https://github.com/Arachnid/EIPs/blob/f6a2640f48026fc06b485dc6eaf04074a7927aef/EIPS/EIP-draft-rich-transactions.md). It lets a signer execute a batch of calls in a single Ethereum Transaction while maintaining msg.sender as the signer of the transaction. It is desirable to streamline the user experience (e.g. one transaction to perform several actions). Our proposal for callWithSigner can achieve a similar effect as the contract code that surrounds the the opcode can be used to send a batch of transactions. For example: ``` for(uint i=0; i +In all cases, if the transaction passes, then it should test that msg.sender is the signer's address: -Replay protection testcases: +Replay protection: - For queue=0, the first nonce=0 is accepted. first nonce=0 for queue=0 is accepted. - For queue=0, the nonce is accepted if it is incremented sequentially. @@ -164,12 +184,12 @@ Replay protection testcases: - For queue=0 and queue=3, the first nonce=0 is accepted. - Replay protection is rejected due to bad encoding (e.g. 3 uints instead of 2) -Signature testcases: +Signature verification: - Signature is valid if the replay protection and target contract/calldata is valid. - It will not verify the user's signature if the replay protection is invalid/used already. -Call testcases. In all cases, if the transaction passes, then it should test that msg.sender is the signer's address: +Call: - Transaction should succeed if the target contract and calldata is executed. - Transaction should succeed if calldata requires more than 1 argument. @@ -183,7 +203,12 @@ We do not yet have an implementation of the new opcode/precompile. But we provid ## Security Considerations -Our proposal is potentially the first opcode/precompile that requires persistent storage which may come with its own security/reliability challenges. Furthermore if the replay protection or signature verification is not implemented correctly, then it can facilitate impersonation and/or replay attacks. The final implementation code should be small and the project is well-scoped, so it should be reasonable to audit. +- First opcode/precompile that requires persistant storage +- Potential impersonation attacks if there is a bug in the signature verification +- Potential replay-attack problems if there is a bug in the replay protection +- Cost for creating a new nonce queue should be greater than re-using an existing nonce queue. + +Given the final implementation code should be relatively small and the project is well-scoped, it should be reasonable to audit. ## Copyright From 22db2ccfb8c24ed7c6445a936c22e89b0b68725a Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Wed, 13 May 2020 13:01:07 +0100 Subject: [PATCH 05/11] readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8537220..4f63804 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,11 @@ const tx = await proxyAccount ); ``` -We have included an additional ```relayer``` class that is an example on how a relayer can take the forward parameters and send it to the network. It also includes functionality for encoding the meta-transaction and sending it in the ```data``` field of a transaction. +We have included an additional ```relayer``` class that is an example on how a relayer can take the forward parameters and send it to the network. + +There is also functionality for encoding the meta-transaction and sending it in the ```data``` field of a transaction: + +``` ### All done! Good work! From e7f8bd8c622c041a39da91c77f601f4ba3ab9061 Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Wed, 13 May 2020 13:10:15 +0100 Subject: [PATCH 06/11] updated EIP --- docs/EIP-proposal.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index 4304e1c..8e08535 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -52,18 +52,17 @@ If both checks pass, then the target contract is invoked with the desired callda For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly increment by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. -We provide the rest of this specification in pseudo-Solidity for ease of reading (and its motivation originates [from here](https://github.com/anydotcrypto/metatransactions/blob/master/src/contracts/account/RelayHub.sol). - For the interface, we propose: ``` -targetContract.callWithSigner(callData, replayProtection, signature, signer) +targetContract.callWithSigner(callData, value, replayProtection, signature, signer) ``` It has the following parameters: - `targetContract`: Address of the target contract. Same as CALL. - `calldata`: Encoded function name and data. Same as CALL. +- `value`: Quantity of ETH to send in WEI. Same as CALL. - `replayProtection`: Encoded replay protection of two nonces (queue and queueNonce) - `signature`: Authorised command signed by the user - `signer`: Externally owned account address @@ -71,10 +70,23 @@ It has the following parameters: We assume the following for the encoding and the signing: - `replayProtection` -> `abi.encode(["uint","uint"],[queue,queueNonce]);` -- `signature` -> `Sign(keccak256(targetContract, callData, replayProtection, chainid))` +- `signature` -> `Sign(keccak256(targetContract, callData, value, replayProtection, chainid))` The additional `chainid` is to verify the signature is for the target blockchain (mainnet/ropsten/etc). +At a high level, the opcode executes as follows: + +- Verify the signer's signature over the target contract, callData, replayProtection and the chainid. (Defensive approach: We check against supplied signer address). +- Verify the signer has sufficient balance for the call (signer.balance > value) +- Decode reply protection for queue and queueNonce. +- Compute signer queue index with queueIndex = H(signer, queue) +- Fetch latest storedNonce for the queue +- Check that queueNonce == storedNonce, if so increment by one and store it. If not, revert. +- Change msg.sender to the signer's address. +- Call into the target contract with the supplied callData and the signer's value in WEI. (value taken from the global account system balanace). + +We provide the rest of this specification in pseudo-Solidity for ease of reading (and its motivation originates [from here](https://github.com/anydotcrypto/metatransactions/blob/master/src/contracts/account/RelayHub.sol). + We assume there is a global mapping of replay protection: ``` @@ -106,9 +118,9 @@ function verifyReplayProtection(address _signer, bytes memory _replayProtection) The opcode needs to verify the signer's signature: ``` -function verifySig(address _targetContract, bytes memory _callData, bytes memory _replayProtection, bytes memory _signature) public view returns (address) { +function verifySig(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature) public view returns (address) { - bytes memory encodedData = abi.encode(_targetContract, _callData, _replayProtection, this.chainid); + bytes memory encodedData = abi.encode(_targetContract, _callData, _value, _replayProtection, this.chainid); return ECDSA.recover(keccak256(encodedData), _signature); } @@ -117,14 +129,14 @@ function verifySig(address _targetContract, bytes memory _callData, bytes memory Altogether the final functionality: ``` -function callWithSigner(address _targetContract, bytes memory _callData, bytes memory _replayProtection, bytes memory _signature, address _signer) public { +function callWithSigner(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature, address _signer) public { require(verifyReplayProtection(_replayProtection, _signer), "Replay protection is not valid"); require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); msg.sender = signer; // Override msg.sender to be signer - _targetContract.call(_callData); + _targetContract.call(_value)(_callData); } ``` @@ -207,6 +219,7 @@ We do not yet have an implementation of the new opcode/precompile. But we provid - Potential impersonation attacks if there is a bug in the signature verification - Potential replay-attack problems if there is a bug in the replay protection - Cost for creating a new nonce queue should be greater than re-using an existing nonce queue. +- We authenticate the blockchain via the ChainID. If two blockchains share the same ChainID, it may facilitate replay attacks. Given the final implementation code should be relatively small and the project is well-scoped, it should be reasonable to audit. From 8c4fbd1a7c30f1d69946bebca68e43d534f71878 Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Wed, 13 May 2020 13:11:30 +0100 Subject: [PATCH 07/11] Added callgroup proposal --- docs/EIP-proposal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index 8e08535..ac3d916 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -168,7 +168,7 @@ for(uint i=0; i Date: Wed, 13 May 2020 13:12:30 +0100 Subject: [PATCH 08/11] Added value check in solidity example --- docs/EIP-proposal.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index ac3d916..b1fe7d5 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -135,6 +135,7 @@ function callWithSigner(address _targetContract, bytes memory _callData, uint _v require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); + require(signer.balance >= _value, "Signer does not have sufficient balance for the call"); msg.sender = signer; // Override msg.sender to be signer _targetContract.call(_value)(_callData); } From dc5f82a163c49f8159863314fe7b6fe7d60a177e Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Wed, 13 May 2020 17:24:55 +0100 Subject: [PATCH 09/11] updated --- docs/EIP-proposal.md | 135 +++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index b1fe7d5..501edf1 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -1,7 +1,7 @@ --- eip: title: CallWithSigner - A new CALL opcode to replace msg.sender with the signer's address -author: +author: Patrick McCorry (@stonecoldpat) & Chris Buckland (@yahgwai) discussions-to: status: draft type: Standards Track @@ -74,74 +74,25 @@ We assume the following for the encoding and the signing: The additional `chainid` is to verify the signature is for the target blockchain (mainnet/ropsten/etc). -At a high level, the opcode executes as follows: - -- Verify the signer's signature over the target contract, callData, replayProtection and the chainid. (Defensive approach: We check against supplied signer address). -- Verify the signer has sufficient balance for the call (signer.balance > value) -- Decode reply protection for queue and queueNonce. -- Compute signer queue index with queueIndex = H(signer, queue) -- Fetch latest storedNonce for the queue -- Check that queueNonce == storedNonce, if so increment by one and store it. If not, revert. -- Change msg.sender to the signer's address. -- Call into the target contract with the supplied callData and the signer's value in WEI. (value taken from the global account system balanace). - -We provide the rest of this specification in pseudo-Solidity for ease of reading (and its motivation originates [from here](https://github.com/anydotcrypto/metatransactions/blob/master/src/contracts/account/RelayHub.sol). - -We assume there is a global mapping of replay protection: - -``` -mapping(bytes32 => uint256) public nonceStore; -``` - -The opcode needs to check the replay protection is valid: +We assume the account nonce store is a simple mapping: ``` -// Check the signer's replay protection is valid -function verifyReplayProtection(address _signer, bytes memory _replayProtection) internal returns(bool) { - - uint queue; uint queueNonce; - (queue, queueNonce) = abi.decode(_replayProtection, (uint256, uint256)); - - // Notice the signer's address and queue computes the index - bytes32 queue = keccak256(abi.encode(_signer, _queue)); - uint256 storedNonce = nonceStore[queue]; - - if(queueNonce == storedNonce) { - nonceStore[index] = storedNonce + 1; - return true; - } - - return false; -} +mapping(bytes32 -> uint) nonceStore; +uint storedNonce = nonceStore(keccak256(signer || queue)); ``` -The opcode needs to verify the signer's signature: - -``` -function verifySig(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature) public view returns (address) { - - bytes memory encodedData = abi.encode(_targetContract, _callData, _value, _replayProtection, this.chainid); - - return ECDSA.recover(keccak256(encodedData), _signature); -} -``` - -Altogether the final functionality: - -``` -function callWithSigner(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature, address _signer) public { +At a high level, the opcode executes as follows: - require(verifyReplayProtection(_replayProtection, _signer), "Replay protection is not valid"); +- Verify the signer's `signature` using the hash of `targetContract`, `callData`, `replayProtection`, `chainid`. Note we assume a defensive approach as we check the signature against the supplied signer's address. +- Verify the signer has sufficient balance for the call (`signer.balance` > `value`) +- Decode `replayProtection` to fetch `queue` and `queueNonce`. +- Compute the signer's queue index as `queueIndex = H(signer || queue)`. +- Fetch latest `storedNonce` for `queueIndex`. +- Check `queueNonce == storedNonce`, if so increment by one and store it. If not, revert. +- Update the msg.sender such that `msg.sender=signer`. +- Perform the call with `targetContract.call(value)(callData);` We assume the value is taken from the global account system balance. - require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); - - require(signer.balance >= _value, "Signer does not have sufficient balance for the call"); - msg.sender = signer; // Override msg.sender to be signer - _targetContract.call(_value)(_callData); -} -``` - -As we can see in the above, the opcode checks the replay protection and the signer's signature before overriding msg.sender and then executing a normal .call(). +We provide a pseudo-implementation in Solidity of the specification in the implementation section. ## Rationale @@ -212,7 +163,63 @@ More tests can and should be added. The above is a small sample for the initial ## Implementation -We do not yet have an implementation of the new opcode/precompile. But we provided an example in pseudo-Solidity for the specification. This should provide clarity on how it can be implemented if this EIP moves to that stage. +We do not yet have an implementation of the new opcode/precompile. But in the following we provide in pseudo-Solidity for the specification. This should provide clarity on how it can be implemented if this EIP moves to that stage. The motivation of the implementation originates [from here](https://github.com/anydotcrypto/metatransactions/blob/master/src/contracts/account/RelayHub.sol). + +We assume there is a global mapping of replay protection: + +``` +mapping(bytes32 => uint256) public nonceStore; +``` + +The opcode needs to check the replay protection is valid: + +``` +// Check the signer's replay protection is valid +function verifyReplayProtection(address _signer, bytes memory _replayProtection) internal returns(bool) { + + uint queue; uint queueNonce; + (queue, queueNonce) = abi.decode(_replayProtection, (uint256, uint256)); + + // Notice the signer's address and queue computes the index + bytes32 queue = keccak256(abi.encode(_signer, _queue)); + uint256 storedNonce = nonceStore[queue]; + + if(queueNonce == storedNonce) { + nonceStore[index] = storedNonce + 1; + return true; + } + + return false; +} +``` + +The opcode needs to verify the signer's signature: + +``` +function verifySig(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature) public view returns (address) { + + bytes memory encodedData = abi.encode(_targetContract, _callData, _value, _replayProtection, this.chainid); + + return ECDSA.recover(keccak256(encodedData), _signature); +} +``` + +Altogether the final functionality: + +``` +function callWithSigner(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature, address _signer) public { + + require(verifyReplayProtection(_replayProtection, _signer), "Replay protection is not valid"); + + require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); + + require(signer.balance >= _value, "Signer does not have sufficient balance for the call"); + msg.sender = signer; // Override msg.sender to be signer + _targetContract.call(_value)(_callData); +} +``` + +As we can see in the above, the opcode checks the replay protection and the signer's signature before overriding msg.sender and then executing a normal .call(). ## Security Considerations From c422e60cc9e1828fee37bd1de2ee71804c5517c9 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 13 May 2020 19:33:35 +0200 Subject: [PATCH 10/11] Some typos --- docs/EIP-proposal.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index 501edf1..f907001 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -11,7 +11,7 @@ created: 2020-05-11 ## Simple Summary -Ethereum transaction's intertwine the identity of who paid for the transaction (tx.gaspayer) and who wants to execute a command (msg.sender). As a result, it is not straight forward for Alice to pay the gas fee on behalf of Bob who wants to execute a command in a smart contract. If this issue can be fixed, then it allows Bob, without any significant hurdles, to outsource his transaction infrastructure to Alice in a non-custodial manner. This EIP aims to alleviate the issue by introducing a new opcode, `callWithSigner()`, in the EVM. +Ethereum transactions intertwine the identity of who paid for the transaction (tx.gaspayer) and who wants to execute a command (msg.sender). As a result, it is not straightforward for Alice to pay the gas fee on behalf of Bob who wants to execute a command in a smart contract. If this issue can be fixed, then it allows Bob, without any significant hurdles, to outsource his transaction infrastructure to Alice in a non-custodial manner. This EIP aims to alleviate the issue by introducing a new opcode, `callWithSigner()`, in the EVM. ## Abstract @@ -25,8 +25,8 @@ There are two existing solutions that try to solve the problem, but they have th **Proxy contract**. Each user has their own proxy contract. They must migrate funds into the proxy contract and all transactions are sent via the proxy contract. As such, the msg.sender is set as the proxy contract address. There are three hurdles for proxy contracts: -1. **Workflow issues.** There is an additional and inconvenient work-flow of deploying a proxy contract and transferring funds. This can hinder adoption as it is not a straight-forward plug & play experience. -2. **Two addresses.** The user now has two addresses which includes the signing address and the proxy contract address. This needs to be managed as part of the user experience and some dapps may need to take it into account. +1. **Workflow issues.** There is an additional and inconvenient work-flow of deploying a proxy contract and transferring funds. This can hinder adoption as it is not a straightforward plug & play experience. +2. **Two addresses.** The user now has two addresses: the signing address and the proxy contract address. This needs to be managed as part of the user experience and some dapps may need to take it into account. 3. **Trust issues.** Users may have trust issues with storing funds in a smart contract due to the additional security risks. Several events including the Parity Wallet Hack exacerbate the problem. Finally there is a subtle problem on how to recover (and migrate away) from the proxy contract if the provider disappears. For example, if the wallet managing access to the proxy contract discontinues its service, but no other service is using the same standard. @@ -35,7 +35,7 @@ Finally there is a subtle problem on how to recover (and migrate away) from the 1. The `Permit()` function is intrusive as it requires the target contract to natively handle replay protection (e.g. verify the user's signature and then increment nonce by 1). -2. `msgSender()` tries to alleviate the contract intrusiveness as a global singleton RelayHub contract is responsible for handling the replay protection. The target contract is only required to replace msg.sender with msgSender() . +2. `msgSender()` tries to alleviate the contract intrusiveness, as a global singleton RelayHub contract is responsible for handling the replay protection. The target contract is only required to replace msg.sender with msgSender() . So far no single approach has achieved wide-spread adoption and this is most evident in [Gnosis Safe](https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol#L193) that implements three solutions to the msg.sender problem. This includes checking a signed message from the externally owned account that is not compatible with Permit(), checking the message hash for uniqueness (i.e. a form of replay protection) and finally checking contract signatures via EIP-1271. @@ -50,7 +50,7 @@ We propose `callWithSigner()` that checks: If both checks pass, then the target contract is invoked with the desired calldata and the signer's address is set as msg.sender. Of course, the new opcode requires long-term storage to keep track of the latest replay protection used (e.g. it is achieved with a single mapping that links the signer's address to the latest nonce). -For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly increment by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. +For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly be incremented by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. For the interface, we propose: @@ -108,7 +108,7 @@ There are two alternative approaches that we describe below. **Modify Ethereum Transaction**. We can modify the structure of an Ethereum Transaction to include a new field for the signer's address, signature & replay protection and the calldata. The EVM can check if the fields are filled in are correct before swapping msg.sender with the signer's address. Of course, if the fields are omitted, then msg.sender == tx.origin. However modifying the structure of an Ethereum Transaction is an intrusive and significant change. It may require all wallets and tooling to upgrade to support the new EIP. -We provide some brief information in regards to related work: +We provide some brief information in regard to related work: [Account abstraction](https://docs.ethhub.io/ethereum-roadmap/ethereum-2.0/account-abstraction/). It removes the distinction of externally owned accounts and contract accounts. In a way, it is similar to the proxy contract approach where the user's funds are stored in the contract wallet and that is the default msg.sender on the network. As a result, this EIP may not be required as there is no such thing as an 'externally owned account' and thus the signer's address is never used as msg.sender. From 416c1737fca2b796b5ff0050cb16bdcafb869d63 Mon Sep 17 00:00:00 2001 From: Patrick McCorry Date: Mon, 15 Jun 2020 11:29:08 +0100 Subject: [PATCH 11/11] Updated to take into account comments --- docs/EIP-proposal.md | 46 +++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/EIP-proposal.md b/docs/EIP-proposal.md index 501edf1..9ee506a 100644 --- a/docs/EIP-proposal.md +++ b/docs/EIP-proposal.md @@ -11,7 +11,7 @@ created: 2020-05-11 ## Simple Summary -Ethereum transaction's intertwine the identity of who paid for the transaction (tx.gaspayer) and who wants to execute a command (msg.sender). As a result, it is not straight forward for Alice to pay the gas fee on behalf of Bob who wants to execute a command in a smart contract. If this issue can be fixed, then it allows Bob, without any significant hurdles, to outsource his transaction infrastructure to Alice in a non-custodial manner. This EIP aims to alleviate the issue by introducing a new opcode, `callWithSigner()`, in the EVM. +Ethereum transactions intertwine the identity of who paid for the transaction (tx.gaspayer) and who wants to execute a command (msg.sender). As a result, it is not straightforward for Alice to pay the gas fee on behalf of Bob who wants to execute a command in a smart contract. If this issue can be fixed, then it allows Bob, without any significant hurdles, to outsource his transaction infrastructure to Alice in a non-custodial manner. This EIP aims to alleviate the issue by introducing a new opcode, `callWithSigner()`, in the EVM. ## Abstract @@ -25,8 +25,8 @@ There are two existing solutions that try to solve the problem, but they have th **Proxy contract**. Each user has their own proxy contract. They must migrate funds into the proxy contract and all transactions are sent via the proxy contract. As such, the msg.sender is set as the proxy contract address. There are three hurdles for proxy contracts: -1. **Workflow issues.** There is an additional and inconvenient work-flow of deploying a proxy contract and transferring funds. This can hinder adoption as it is not a straight-forward plug & play experience. -2. **Two addresses.** The user now has two addresses which includes the signing address and the proxy contract address. This needs to be managed as part of the user experience and some dapps may need to take it into account. +1. **Workflow issues.** There is an additional and inconvenient work-flow of deploying a proxy contract and transferring funds. This can hinder adoption as it is not a straightforward plug & play experience. +2. **Two addresses.** The user now has two addresses: the signing address and the proxy contract address. This needs to be managed as part of the user experience and some dapps may need to take it into account. 3. **Trust issues.** Users may have trust issues with storing funds in a smart contract due to the additional security risks. Several events including the Parity Wallet Hack exacerbate the problem. Finally there is a subtle problem on how to recover (and migrate away) from the proxy contract if the provider disappears. For example, if the wallet managing access to the proxy contract discontinues its service, but no other service is using the same standard. @@ -35,7 +35,7 @@ Finally there is a subtle problem on how to recover (and migrate away) from the 1. The `Permit()` function is intrusive as it requires the target contract to natively handle replay protection (e.g. verify the user's signature and then increment nonce by 1). -2. `msgSender()` tries to alleviate the contract intrusiveness as a global singleton RelayHub contract is responsible for handling the replay protection. The target contract is only required to replace msg.sender with msgSender() . +2. `msgSender()` tries to alleviate the contract intrusiveness, as a global singleton RelayHub contract is responsible for handling the replay protection. The target contract is only required to replace msg.sender with msgSender() . So far no single approach has achieved wide-spread adoption and this is most evident in [Gnosis Safe](https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol#L193) that implements three solutions to the msg.sender problem. This includes checking a signed message from the externally owned account that is not compatible with Permit(), checking the message hash for uniqueness (i.e. a form of replay protection) and finally checking contract signatures via EIP-1271. @@ -50,19 +50,19 @@ We propose `callWithSigner()` that checks: If both checks pass, then the target contract is invoked with the desired calldata and the signer's address is set as msg.sender. Of course, the new opcode requires long-term storage to keep track of the latest replay protection used (e.g. it is achieved with a single mapping that links the signer's address to the latest nonce). -For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly increment by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. +For the replay protection, we propose using [MultiNonce](https://github.com/PISAresearch/metamask-comp/tree/master#multinonce). Conceptually, the user has a list of nonce queues and in each queue the nonce must strictly be incremented by one. MultiNonce supports up to N concurrent transactions at any time and potentially requires the same storage as a single nonce queue. For the interface, we propose: ``` -targetContract.callWithSigner(callData, value, replayProtection, signature, signer) +targetContract.callWithSigner(gasLimit)(callData, replayProtection, signature, signer) ``` It has the following parameters: - `targetContract`: Address of the target contract. Same as CALL. +- `gasLimit`: Maximum allocation of gas. Same as CALL. - `calldata`: Encoded function name and data. Same as CALL. -- `value`: Quantity of ETH to send in WEI. Same as CALL. - `replayProtection`: Encoded replay protection of two nonces (queue and queueNonce) - `signature`: Authorised command signed by the user - `signer`: Externally owned account address @@ -70,7 +70,7 @@ It has the following parameters: We assume the following for the encoding and the signing: - `replayProtection` -> `abi.encode(["uint","uint"],[queue,queueNonce]);` -- `signature` -> `Sign(keccak256(targetContract, callData, value, replayProtection, chainid))` +- `signature` -> `Sign(keccak256(targetContract, callData, replayProtection, gasLimit, chainid))` The additional `chainid` is to verify the signature is for the target blockchain (mainnet/ropsten/etc). @@ -83,14 +83,14 @@ uint storedNonce = nonceStore(keccak256(signer || queue)); At a high level, the opcode executes as follows: -- Verify the signer's `signature` using the hash of `targetContract`, `callData`, `replayProtection`, `chainid`. Note we assume a defensive approach as we check the signature against the supplied signer's address. -- Verify the signer has sufficient balance for the call (`signer.balance` > `value`) +- Verify the signer's `signature` using the hash of `targetContract`, `callData`, `replayProtection`, `gasLimit`, `chainid`. Note we assume a defensive approach as we check the signature against the supplied signer's address. +- Verify sufficient gas is available for the call (`gasleft()` > `gasLimit`) - Decode `replayProtection` to fetch `queue` and `queueNonce`. - Compute the signer's queue index as `queueIndex = H(signer || queue)`. - Fetch latest `storedNonce` for `queueIndex`. - Check `queueNonce == storedNonce`, if so increment by one and store it. If not, revert. - Update the msg.sender such that `msg.sender=signer`. -- Perform the call with `targetContract.call(value)(callData);` We assume the value is taken from the global account system balance. +- Perform the call with `targetContract.call(gasLimit)(callData);` We provide a pseudo-implementation in Solidity of the specification in the implementation section. @@ -104,11 +104,11 @@ The rationale to favor a new opcode is the following: There are two alternative approaches that we describe below. -**Pre-signed Ethereum Transaction.** It is possible to supply a pre-signed Ethereum Transaction to the new opcode. We can re-use the existing account system for the replay protection and re-use significant portions of code (both in the node and client-side) to handling the transaction. However, it does involve a more complicated data-structure (e.g. RLP decoding, additional fields, etc) and it may not be desirable to mix the replay protection of both systems. As well, it limits the signer to NONCE replay protection (single queue). We mention the approach as it is a desirable alternative that should be considered and it has been implemented in the [GSN](https://github.com/opengsn/gsn/blob/master/contracts/Penalizer.sol#L28). +**Pre-signed Ethereum Transaction.** It is possible to supply a pre-signed Ethereum Transaction to the new opcode. We can re-use the existing account system for the replay protection and re-use significant portions of code (both in the node and client-side) to handling the transaction. However, it does involve a more complicated data-structure (e.g. RLP decoding, additional fields, etc) and it may not be desirable to mix the replay protection of both systems. As well, it limits the signer to NONCE replay protection (single queue). We mention the approach as it is a desirable alternative that should be considered and it has been implemented in the [GSN](https://github.com/opengsn/gsn/blob/master/contracts/Penalizer.sol#L28). Note this can break the [tx pool invariant](https://ethereum-magicians.org/t/eip-callwithsigner-as-a-potential-fix-for-the-msg-sender-problem/4340/2). **Modify Ethereum Transaction**. We can modify the structure of an Ethereum Transaction to include a new field for the signer's address, signature & replay protection and the calldata. The EVM can check if the fields are filled in are correct before swapping msg.sender with the signer's address. Of course, if the fields are omitted, then msg.sender == tx.origin. However modifying the structure of an Ethereum Transaction is an intrusive and significant change. It may require all wallets and tooling to upgrade to support the new EIP. -We provide some brief information in regards to related work: +We provide some brief information in regard to related work: [Account abstraction](https://docs.ethhub.io/ethereum-roadmap/ethereum-2.0/account-abstraction/). It removes the distinction of externally owned accounts and contract accounts. In a way, it is similar to the proxy contract approach where the user's funds are stored in the contract wallet and that is the default msg.sender on the network. As a result, this EIP may not be required as there is no such thing as an 'externally owned account' and thus the signer's address is never used as msg.sender. @@ -196,10 +196,9 @@ function verifyReplayProtection(address _signer, bytes memory _replayProtection) The opcode needs to verify the signer's signature: ``` -function verifySig(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature) public view returns (address) { - - bytes memory encodedData = abi.encode(_targetContract, _callData, _value, _replayProtection, this.chainid); +function verifySig(address _targetContract, bytes memory _callData, uint _gasLimit, bytes memory _replayProtection, bytes memory _signature,) public view returns (address) { + bytes memory encodedData = abi.encode(_targetContract, _callData, uint _gasLimit, _replayProtection, this.chainid); return ECDSA.recover(keccak256(encodedData), _signature); } ``` @@ -207,15 +206,14 @@ function verifySig(address _targetContract, bytes memory _callData, uint _value, Altogether the final functionality: ``` -function callWithSigner(address _targetContract, bytes memory _callData, uint _value, bytes memory _replayProtection, bytes memory _signature, address _signer) public { +function callWithSigner(address _targetContract, bytes memory _callData, uint _gasLimit, bytes memory _replayProtection, bytes memory _signature, address _signer) public { require(verifyReplayProtection(_replayProtection, _signer), "Replay protection is not valid"); - - require(signer == verifySignature(_targetContract, _callData, _replayProtection, _signature), "Signer did not authorise this command"); - - require(signer.balance >= _value, "Signer does not have sufficient balance for the call"); + require(signer == verifySignature(_targetContract, _callData, uint _gasLimit, _replayProtection, _signature), "Signer did not authorise this command"); msg.sender = signer; // Override msg.sender to be signer - _targetContract.call(_value)(_callData); + + // We assume that .call() checks gasleft() > gasLimit (although it has quirks due to EIP-150) + _targetContract.call(_gasLimit)(_callData); } ``` @@ -234,3 +232,7 @@ Given the final implementation code should be relatively small and the project i ## Copyright Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). + +## Acknowledgements + +Thanks to [for proposing to commit to gasLimit](https://ethereum-magicians.org/t/eip-callwithsigner-as-a-potential-fix-for-the-msg-sender-problem/4340/3) and [for pointing out the txpool problem if we used the account system](https://ethereum-magicians.org/t/eip-callwithsigner-as-a-potential-fix-for-the-msg-sender-problem/4340/2).