diff --git a/neps/nep-0518.md b/neps/nep-0518.md new file mode 100644 index 000000000..7ea39b676 --- /dev/null +++ b/neps/nep-0518.md @@ -0,0 +1,221 @@ +--- +NEP: 518 +Title: Web3-Compatible Wallets Support +Authors: Aleksandr Shevchenko , Michael Birch +Status: New +DiscussionsTo: https://github.com/near/NEPs/issues/518 +Type: Protocol +Version: 1.0.0 +Created: 2023-11-15 +LastUpdated: 2024-07-22 +--- + +## Summary + +This NEP describes the protocol changes needed to support the usage of Ethereum-compatible wallets (Web3 wallets), for example Metamask, on Near native applications. That is to say, with this protocol change all Metamask users can become Near users without installing any additional software; from their perspective Near will appear as just another network they can choose from (similar to Aurora today). + +This is accomplished through two key protocol changes: + +1. Ethereum-like addresses (i.e. account IDs of the form `^0x[a-f0-9]{40}$`) are implicit accounts on Near (i.e. can be created via a `Transfer` action). We call these "eth-implicit accounts". +2. Unlike the current implicit accounts (64-character hex-encoded), eth-implicit accounts do not have any access keys added to them on creation. Instead, these accounts will have a special contract deployed to them automatically called the "wallet contract". This wallet contract enables the owner of the Ethereum address corresponding to the eth-implicit account ID to sign transactions with their Ethereum private key, thus providing similar functionality to the default access key of 64-character implicit accounts. + +The nature of this NEP requires the reader to know some concepts from the Ethereum ecosystem. However, since this is a document for readers only familiar with the Near network, we include appendices with definitions and descriptions of the Ethereum concepts needed to understand this proposal. Terms in bold, for example **EOA**, are defined in the glossary (Appendix A). + +The protocol changes described here are a part of the overall eb3-Compatible Wallets Support solution. The full solution (including the protocol changes described here) are detailed in the original [NEP-518 issue description](https://github.com/near/NEPs/issues/518). + +## Motivation + +Currently, the Ethereum ecosystem is a leading force in the smart contract blockchain space, boasting a large user base and extensive installations of Ethereum-compatible tooling and wallets. However, a significant challenge arises due to the incompatibility of these tools and wallets with NEAR Protocol. This incompatibility necessitates a complete onboarding process for users to interact with NEAR contracts and accounts, leading to confusion, decreased adoption, and the marginalization of NEAR Protocol. + +Implementing Web3 wallet support in NEAR Protocol, with an emphasis on user experience continuity, would significantly benefit the entire NEAR Ecosystem. + +## Specification + +### Eth-implicit accounts + +**Definition**: An eth-implicit account is a top-level Near account with ID of the form `^0x[a-f0-9]{40}$` (i.e. 42-characters with `0x` as a prefix followed by 40 characters of hex-encoded data which represents a 20-byte address). + +Eth-implicit accounts, as the name suggests, are implicit accounts on Near. This means if the target account ID does not exist during a `Transfer` action then it MUST be automatically created. This includes being created even if the amount being transferred is zero (per the prior [NEP on zero balance accounts](https://github.com/near/NEPs/blob/master/neps/nep-0448.md)). Eth-implicit accounts represent an Ethereum **EOA** and therefore are controlled via the Ethereum private key corresponding to the address contained in the account ID (see Appendix B for a description of how 20-byte addresses are derived from a private key in the Ethereum ecosystem). To enable this control, eth-implicit accounts all have a smart contract deploy to them called the wallet contract (specification in the next section). + +When an eth-implicit account is created the runtime MUST set the contract code equal to specific "magic bytes". These bytes come from a UTF-8 encoded string which is equal to the constant `near` appended with the base-58 encoding of the sha2 hash of the wallet contract code. This constant allows the contract runtime to lookup the full contract code without needing it to be stored multiple times in the state. As well as being more efficient for the protocol, setting the code equal to a hash of the contract instead of the contract itself keeps the storage requirements of a new eth-implicit account small enough to be a zero balance account. + +The magic bytes depend on the Near network chain id because the wallet contract (and therefore its hash) depends on the Near chain id. The magic bytes (UTF-8 encoded) for each Near chain id are listed below: + +- `mainnet`: `near83PPBGX9KNgC2TRJgX7mvZfFPx92bFkdYvZNARQjRt8G` +- `testnet`: `near3Za8tfLX6nKa2k4u2Aq5CRrM7EmTVSL9EERxymfnSFKd` +- any other id (e.g. `localnet`): `near2dQzuvePVCmkXwe1oF3AgY9pZvqtDtq43nFHph928CU4` + +When the runtime is executing a `FunctionCall` action on an account with these magic bytes as code then it MUST act as if the wallet contract code were stored there instead (i.e. the wallet contract Wasm module ends up being executed). + +### Wallet contract + +This smart contract is automatically deployed to all eth-implicit accounts (see prior section). The purpose of this contract is to accept transactions encoded in an Ethereum style and create Near actions which are executed in subsequent receipts. In this way, the owner of the Ethereum private key associated with the eth-implicit account (the address contained in its account ID) controls what actions the account takes. Thus that Ethereum key effectively becomes the only access key for the account, emulating the behavior of an Ethereum **EOA**. + +#### API + +The wallet contract has two public functions: + +- `get_nonce` is a view function which takes no arguments and returns a 64-bit number (encoded as a base-10 string). +- `rlp_execute` is the main entry point for executing user transactions. It takes two inputs (encoded as a JSON object): `target` is an account ID (i.e. string) which indicates the account that is supposed to be the target of the Near action; and `tx_bytes_b64` is a string which is the base-64 encoding of the raw bytes of an Ethereum-like transaction. The process by which a Near action is derived from the Ethereum transaction is described below. + +The wallet contract has two state variables: the nonce, a 64-bit number; and a boolean flag indicating if a transaction is currently in progress. As with nonce values on Near access keys, the purpose of the wallet contract nonce is to prevent replaying the same Ethereum transaction more than once. The boolean flag prevents multiple transactions from being in-flight at the same time. The reason this is needed is because of the asynchronous nature of Near as compared with the synchronous nature of the **EVM**. On Ethereum if two transactions are sent (they must have sequential nonces per the Ethereum standard) all actions of the first will happen before all actions of the second. However, on Near there is no guarantee of the order of execution for receipts in different shards. Therefore, the only way to ensure that all actions from the first transaction are executed before all the actions of the second transaction is to prevent the second transaction from starting its execution until after the first one entirely finishes. + +#### Details of `rlp_execute` + +This function is named after the **RLP** standard in Ethereum. In particular, the `tx_bytes_b64` argument is parsed into bytes from base-64; then the bytes are parsed into structured data assuming it is RLP encoded; then the structured data is parsed into an Ethereum transaction. Ethereum transactions can have multiple different forms since the Ethereum protocol has evolved over time (there are "legacy" transactions, [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) type transactions, [EIP-2930](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2930.md) transactions). All these different forms are supported by the wallet contract (they are distinguished based on the "type byte" which starts the encoding as per [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md)) and are ultimately all transformed into a common data structure with the following fields: + +- `from`: the address associated with the private key that signed the transaction. +- `chain_id`: a numerical ID that is unique per **EVM**-chain. The Near chain ID values are discussed below. +- `nonce`: the nonce associated with this transaction. It must be equal to the wallet contracts's currently stored nonce for the transaction to be executed. +- `gas_limit`: the maximum amount of **EVM** gas the user is willing to spend on this transaction. +- `max_fee_per_gas`: the gas price the user is willing to pay. `gas_limit * max_fee_per_gas` gives the maximum amount of **Wei** the user is willing to pay for the transaction. +- `to`: the address of the account the transaction is targeting. This could be another **EOA** in the case of a base token transfer or the address of a smart contract in the case of what Near would refer to as a function call. In the Ethereum standard this field is allowed to be empty to indicate a new contract is being created, however that is forbidden by the wallet contract because Near currently does not support **EVM** bytecode, so there is not a reasonable way to emulate an Ethereum contract deployment. +- `value`: the amount of **Wei** attached to the transaction. +- `data`: the raw bytes which will be sent as a payload to the target address. If the target address is a contract it will use these bytes as input. + +Note: some Ethereum transaction fields are intentionally omitted because they are unused by the wallet contract. + +These fields are used to validate the transaction and derive Near actions that the wallet contract will create as receipts. The details of this process are described below. + +##### Ethereum transaction validation + +The following validation conditions MUST pass for the wallet contract to accept a transaction. + +1. `from` address when formatted as hex-encoded with `0x` prefix MUST match the current account ID (i.e. the wallet contract's account ID). +2. `chain_id` MUST match one of the following values depending on the Near chain the wallet contract is deployed to: mainnet -> 397; testnet -> 398; any other chain -> 399. The mainnet and testnet values are registered with the [official Ethereum ecosystem registry of chain IDs](https://github.com/ethereum-lists/chains). +3. `nonce` MUST match the nonce value currently stored in the contract state. +4. `to` address MUST either (a) be equal to `keccak256(target)[12,32]` (where `target` is other argument passed to the `rlp_execute` function) or (b) when `to` is formatted as hex-encoded with `0x` prefix it MUST be equal to `target`. In case (b) there is an additional validation check that the `to` address is not registered in the "Ethereum Translation Contract" (ETC). The details of this check and why it is needed are discussed in Appendix C. +5. `value` MUST be less than or equal to `(2**128 - 1) // 1_000_000`. This condition arises from the mismatch is decimal places between Ether and NEAR which is discussed in the definition of **Wei** in Appendix A. Essentially, we must ensure the `value` can be mapped into a valid amount of yoctoNEAR, which means `value * 1_000_000 <= u128::MAX`. + +##### Converting Ethereum transaction into Near actions + +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 that 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 + +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 + +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. + +These protocol changes are implemented in nearcore ([eth-implicit accounts PR 10224](https://github.com/near/nearcore/pull/10224), [wallet contract implementation](https://github.com/near/nearcore/tree/1ab9b42c3d723604a214e685d8ed39f7d6434ae2/runtime/near-wallet-contract/implementation)) and have been stabilized in protocol version 70 ([PR 11765](https://github.com/near/nearcore/pull/11765)). + +## Security Implications + +The wallet contract must uphold the invariant that only the owner of the private key can make the wallet contract create Near actions. The wallet contract has been audited and is believed to be secure. + +## Alternatives + +See the "Prior work" section of the [original NEP-518 issue](https://github.com/near/NEPs/issues/518). + +## Future possibilities + +See the "Future Opportunities" section of the [original NEP-518 issue](https://github.com/near/NEPs/issues/518). + +## Consequences + +### Positive + +- All Ethereum users can easily onboard to Near + +### Neutral + +- New implicit account type with a protocol-level smart contract deployed by default. + +### Backwards Compatibility + +As pointed out in [PR 11606](https://github.com/near/nearcore/pull/11606) there are 5552 accounts on mainnet today with account IDs that would classify them as eth-implicit accounts. For backwards compatibility, these accounts will not be changed in any way (their access keys and contract code will be left in place) and therefore will in fact still be normal Near accounts as opposed to eth-implicit accounts because they have full access keys and possibly a contract different from the protocol-sanctioned wallet contract. + +## Appendix A - Glossary + +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. + +## Appendix B - How addresses are derived in Ethereum + +On Ethereum all accounts are identified by a 20-byte address. The address of a user account is derived from a user's private key in the following way: + +1. Compute the user's public key from the private key (this step can be omitted if you already, indeed only, know the public key). +2. Compute the keccak256 hash of the public key. +3. Return the rightmost 20 bytes of this hash. + +This is summarized by the following formula: `address = keccak256(public_key)[12,32]`. + +## Appendix C - Ethereum Translation Contract (ETC) + +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 + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).