Skip to content

Commit

Permalink
Complete missing TODOs
Browse files Browse the repository at this point in the history
  • Loading branch information
birchmd committed Jul 29, 2024
1 parent c297531 commit c090b75
Showing 1 changed file with 68 additions and 5 deletions.
73 changes: 68 additions & 5 deletions neps/nep-0518.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
NEP: 518
Title: Web3-Compatible Wallets Support
Authors: Aleksandr Shevchenko <[email protected]>
Authors: Aleksandr Shevchenko <[email protected]>, Michael Birch <[email protected]>
Status: New
DiscussionsTo: https://github.com/near/NEPs/issues/518
Type: Protocol
Expand Down Expand Up @@ -89,15 +89,66 @@ The following validation conditions MUST pass for the wallet contract to accept

##### Converting Ethereum transaction into Near actions

TODO
Each Ethereum transaction is converted to a single Near action (batch transactions are not supported) based on the `data` field. Following the Solidity convention of the first four bytes of the data being a **method selector**, the wallet contract checks the first four bytes of the `data` to see if it is a known Near action. The **method selectors** for Near actions supported by the wallet contract are determined by mapping the actions to an equivalent Solidity function signature as follows:

- `functionCall(string,string,bytes,uint64,uint32)`
- `transfer(string,uint32)`
- `addKey(uint8,bytes,uint64,bool,bool,uint128,string,string[])`
- `deleteKey(uint8,bytes)`

Note that the `uint32` fields in `functionCall` and `transfer` contain the amount of yoctoNEAR tha cannot be included in the Ethereum transaction's `value` field due to the difference in decimal places (see **Wei** definition in Appendix A), therefore the value there is always less than `1_000_000` so it will easily fit in a 32-bit number. These type signatures then hash to the **method selectors**:

- FunctionCall: `0x6179b707`
- Transfer: `0x3ed64124`
- AddKey: `0x753ce5ab`
- DeleteKey: `0x3fc6d404`

If the first four bytes of the `data` field matches one of these **method selectors** then the wallet contract will try to parse the remainder of the `data` into the corresponding type signature (assuming the data is Solidity ABI encoded). If this parsing succeeds then the resulting tuple of values can be converted to the corresponding Near action. Some additional validation is done in this case, depending on the action:

- FunctionCall/Transfer: `target` MUST equal the first `string` parameter (interpreted as the receiver ID), the `uint32` parameter value MUST be less than `1_000_000`.
- AddKey/DeleteKey: the `uint8` parameter value MUST be 0 (corresponding to an ED25519 access key) or 1 (corresponding to a Secp256k1 access key), the `bytes` MUST be the appropriate length depending on the key type, `target` MUST equal the current account ID (since these actions can only act on the current account).

Additionally, the first `bool` value of `addKey` must be `false` because adding a full access key is currently not supported by the wallet contract. The reason for this is to prevent users from changing the contract code deployed to the eth-implicit contract, as it could break the account's intended functionality. However, this restriction may be lifted in the future.

If the first four bytes of `data` does not match one of these known selectors then the contract tries another set of known **method selectors** which come from the Ethereum ERC-20 standard:

- `balanceOf(address)` -> `0x70a08231`
- `transfer(address,uint256)` -> `0xa9059cbb`
- `totalSupply()` -> `0x18160ddd`

These **method selectors** are included because some Web3 wallets (for example MetaMask) allow a user to transfer tokens directly within the wallet interface. This interface produces an Ethereum transaction with Solidity ABI encoded data following the ERC-20 standard rather than the encoding of the Near actions outlined above. Therefore the wallet contract also knows how to parse these ERC-20 standard methods into Near actions so that the wallet interfaces still work according to the user's expectations. This feature of the wallet contract is called Ethereum Standards Emulation because it emulates the execution of an Ethereum standard. Currently ERC-20 is the only supported standard for emulation, but perhaps more will be added in the future.

ERC-20 is Ethereum's fungible token standard, thus these calls are mapped to the corresponding NEP-141 `FunctionCall` actions:

- `balanceOf` -> `ft_balance_of`
- `transfer` -> `ft_transfer`
- `totalSupply` -> `ft_total_supply`

Note: it is intentional that not all the ERC-20 functions are emulated (in particular related to `approve`/`allowance`) because there is not the corresponding functionality in NEP-141. There is additional validation in the case of `transfer` that the amount is less than `u128::MAX` because the ERC-20 standard allows 256-bit amounts while the NEP-141 standard only allows 128-bit. The NEP-141 standard also has additional complexity that ERC-20 does not have because of the storage deposit requirement (a consequence of Near's storage staking). On Ethereum a user can transfer tokens to another account that has never held that kind of token before. On Near that is only possible if the user pays for the recipient's storage deposit first. Therefore, as part of the `transfer` emulation the wallet contract includes a call to `storage_balance_of` to check if a call to `storage_storage_deposit` is also needed before calling `ft_transfer`.

If none of the known selectors match the first four bytes of `data` or the remainder of `data` fails to parse into the appropriate type signature then there is one more possible emulation that the wallet contract checks for. On Ethereum base token transfers are allowed to have arbitrary data included and some wallets use this feature as a sort of messaging protocol between addresses. Therefore, if the `data` is not processed and the `target` is another eth-implicit account, then we assume this is meant to emulate a base token transfer and thus a Near `Transfer` action is created. Otherwise, the wallet contract returns an error that the transaction could not be parsed.

#### Interaction with Web3 relayers

TODO
Typically users will not be constructing the `rlp_execute` action themselves because the target user group are those who only have a Web3 wallet like MetaMask, not a Near wallet to sign Near transactions. Therefore, the Near transactions will be constructed and sent to the Near network on a user's behalf by relayers. These relayers expose the Ethereum standard JSON RPC so that Web3 wallets know how to as the relayer to send an Ethereum-like transaction and to query the status of that transaction. More details about relayers and the RPC they expose is found in the [NEP-518 issue description](https://github.com/near/NEPs/issues/518), but it out of scope for this document because they operated separately from the Near protocol itself.

The relevant fact for the wallet contact specification is that relayers can ask their users to add a function call access key to their eth-implicit account which the relayer uses to call `rlp_execute`. By using an access key on the eth-implicit account itself, the relayer does not need to cover any gas costs for the user because the transaction originates from the wallet contract account itself. However, for this mechanism to be safe for users, relayers must be prevented from sending transactions to the wallet contract that the user did not intend. Otherwise relayers could maliciously burn the $NEAR of their users on excess calls to `rlp_execute` (even if those transactions return an error, gas is still spent in the process).

For this reason, the wallet contract separates possible errors in the `rlp_execute` input into two categories: user errors and relayer errors. User errors are errors that arise from data signed by the user's private key and therefore cannot be spoofed by the relayer. Relayer errors arise from input that should not have been sent by an honest relayer in the first place. These relayer errors include:

- Invalid Ethereum transaction nonce: if the nonce check fails then the relayer is at fault because it should have checked the nonce before sending the transaction. This prevents a malicious relayer from sending the same user-signed transaction over and over to burn the user's $NEAR unnecessarily.
- Invalid base-64 encoding in `tx_bytes_b64`: an honest relayer should only send valid arguments. If a relayer sends garbage input then it is faulty.
- Invalid Ethereum transaction encoding: similar to the error above, but with the issue occurring in the RLP-encoding instead of in the base-64 encoding.
- Invalid sender: if the address extracted from the signature on the Ethereum transaction does not match the wallet contract account ID then the relayer is faulty because it sent an incorrectly signed transaction.
- Invalid target: if the `target` validation relative to the `to` field in the user's signed Ethereum transaction fails then the relayer is faulty because it tried to misdirect the transaction to a different account than the user intended.
- Invalid chain id: similar to the invalid sender error, the relayer should only send transaction with a valid signature, including with the correct chain id.
- Insufficient gas: if the relayer does not attach as much Near gas to the transaction as the user asked for in the `gas_limit` field of their signed Ethereum transaction then it is faulty. This prevents a malicious relayer from intentionally making user transactions fail by not attaching enough gas to complete the action.

If a relayer error happens then the wallet contract creates a callback to remove the relayers access key. This prevents them from repeatedly sending incorrect input.

## Reference implementation

The protocol changes necessary for the project include:
Summarizing the above, the protocol changes necessary for the Web3 wallets project include:

- Creating Ethereum-like (0x) implicit accounts using `Transfer` action,
- Automatically deploying the wallet contract to those 0x implicit accounts.
Expand Down Expand Up @@ -136,6 +187,7 @@ Below is a list of Ethereum-related terms and their definitions.

- **Ethereum Virtual Machine (EVM)**: the virtual machine used to execute smart contracts on the Ethereum blockchain. "EVM-compatible" is often used interchangeably with "Ethereum compatible".
- **Externally owned account (EOA)**: An Ethereum account for which a user has the private key. Unlike Near, on Ethereum there is a distinction between contracts and user accounts. User accounts cannot have contract code and contract accounts cannot initiate a transaction.
- **Method selector**: By convention in Solidity contracts, the first four bytes of the input to a smart contract determine which method is executed (unlike Near where a method is explicitly specified as part of the `FunctionCall` action). These bytes are obtained by taking the first four bytes of the keccak256 hash of the type signature of the function.
- **Recursive Length Prefix (RLP) serialization**: An Ethereum ecosystem standard for encoding structured data as bytes. It plays a similar role to `borsh` in the Near ecosystem.
- **Wei**: the smallest unit of the base token for Ethereum. It plays a similar role to yoctoNEAR in the Near ecosystem. An important difference between Wei and yoctoNEAR is that 1 Ether (the typical unit for the base token on Ethereum) is equal to 10^18 Wei, while 1 NEAR is equal to 10^24 yoctoNEAR. Phrased another way, Ether has 18 decimal places while NEAR has 24. This difference in precision creates minor complexities in the wallet contract.

Expand All @@ -151,7 +203,18 @@ This is summarized by the following formula: `address = keccak256(public_key)[12

## Appendix C - Ethereum Translation Contract (ETC)

TODO
There is an additional contract which is tangentially related to the wallet contract. The [original NEP-518 issue](https://github.com/near/NEPs/issues/518) refers to it as the Ethereum Translation Contract (ETC), though perhaps a more descriptive name is the Ethereum address registrar. The implementation of this contract is not part of the protocol, however its account ID is because the account ID is hardcoded into the wallet contract. The reason is because the wallet contract occasionally needs the ETC to verify if the `target` argument to `rlp_execute` is properly set relative to the `to` field of the user's signed Ethereum transaction. The details of why ETC is needed and how it is used is described below.

Recall that the user is signing an Ethereum transaction because the whole point of this project is to allow Web3 wallets like MetaMask to be used on Near. An Ethereum transaction specifies the target of a transaction using a 20-byte address because there are no named accounts on Ethereum. Therefore the user only signs over a 20-byte address to indicate their intent of what account is meant to receive this transaction. However, this is obviously insufficient information on Near because most accounts are named ones, not addresses. The purpose of the `target` argument to `rlp_execute` is to communicate the account ID of the receiver of the transaction and it must be consistent with the user's signed Ethereum transaction according to the validation conditions described in the "Ethereum transaction validation" section.

Most of the time that means checking `to == keccak256(target)[12,32]` because the `target` will be some named Near account. However, it is possible that `target` be another eth-implicit account; this is the case for "emulated" base token transfers (emulated Ethereum standards are discussed in the section "Converting Ethereum transaction into Near actions"). Thus, we must also allow the possibility that `to == target`. Yet, this poses a problem because it means `target` could be set incorrectly if it was meant to be a named account satisfying the hash condition instead. The ETC closes the loophole by providing a reverse lookup from 20-byte address to named Near accounts where the association comes from the hash condition.

To fully validate `target` in the case that `to == target` the wallet contract makes the following additional checks:

- If the `data` field of the user's signed Ethereum transaction can be parsed into a Near action then confirm `target` matches the `receiver_id` of the corresponding action (this is statically known to be the current account ID in the case of `AddKey` and `DeleteKey`, and it is encoded with the action in the case of `FunctionCall` and `Transfer`).
- If the `data` field can be parsed as ERC-20 action then call the `lookup` method of the ETC to see if the `target` is registered. If it is registered then the `target` field is set incorrectly because the relayer should have set `target` equal to the named account returned from `ETC::lookup(to)`. This validity check ensures that emulated ERC-20 transactions are sent to the correct NEP-141 token account. If `target` is not registered then the transaction is interpreted as an emulated base token transfer with a message that happens to parse like an ERC-20 function call.

Notably, for this security measure to be effective all widely used NEP-141 token accounts will need to be registered with ETC. ETC has a public method `register` which permissionlessly allows anyone to add an account ID they think is important. This openness id not a feasible attack vector for the system because of the one-way nature of the keccak256 hash function preventing an attacker from coming up with a Near account ID that corresponds to an address of their choosing.

## Copyright

Expand Down

0 comments on commit c090b75

Please sign in to comment.