Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEP-518] Web3-Compatible Wallets Support #555

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions neps/nep-0518.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
NEP: 518
Title: Web3-Compatible Wallets Support
Authors: Aleksandr Shevchenko <alex.shevchenko@aurora.dev>
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible in future for me to "deploy" a different contract on my ETH implicit account? Could there be interesting use cases for such a feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it is impossible to deploy a different contract to a new eth implicit account. This could be enabled if the wallet contract was upgraded to allow users to take the DeployContract action or if the wallet contract allowed adding full access keys. However, this is a dangerous feature because it could result in users no longer being able to control their account using Ethereum-like transactions.

I cannot think of good use cases where the benefits of being able to deploy a different contract are worth the risk of a user accidentally locking themselves out of their account. But if there is one in the future then we can consider upgrading the wallet contract in another protocol change.


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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this scheme be extended in future if we need to add support for different types of account ids?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear how this could be extended to support other types of account IDs.

During the design we thought about having a chain-specifying prefix (e.g. eth), or having the addresses be sub-accounts of a chain-specifying account (again e.g. eth), but we ultimately chose to just have the account ID be equal to the address. The reasons for this choice are (1) with the number of EVM chains that exist now it doesn't really make sense to have a chain specifier with an address because they exact same key could control the same address on multiple chains (e.g. Etheruem, Aurora, Arbitrum, BNB, etc) -- including now the Near chain with this proposal; (2) it simplifies how users can look up their accounts in Near explorers (they just search for their address exactly as they would in etherscan for example, though with the caveat that they cannot use the address checksum casing because Near requires account IDs to be lowercase).


Eth-implicit accounts, as the name suggests, are implicit accounts on Near. This means MUST be automatically created by a `Transfer` action if the target account ID does not exist. 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).
birchmd marked this conversation as resolved.
Show resolved Hide resolved
akhi3030 marked this conversation as resolved.
Show resolved Hide resolved

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, this setting the code equal to a hash of the contract instead of the contract itself is what keeps the storage requirements of a new eth-implicit account small enough to be a zero balance account.
birchmd marked this conversation as resolved.
Show resolved Hide resolved

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) then the all actions of the first will always happen before all the 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.
birchmd marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's explained further below but leaving a comment for now in case I forget. How is the boolean flag used exactly? What happens when it indicates that a transaction is in flight? Users cannot control in which order their transactions are getting processed, will their contracts be rejected arbitrarily? Is it possible that a user has submited transactions 1,2,3. 1 is being processed so 2 gets rejected and then when 1 is processed, 3 arrives and gets processed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If rlp_execute is called while the boolean flag is set (so there is already a transaction in flight) then that rlp_execute call will immediately fail with an error about not allowing simultaneous transactions.

In your example where transaction 2 is rejected because 1 is still executed then 3 will also be rejected because the wallet contract follows the Ethereum standard of requiring nonces to be sequential. Transaction 3 will be rejected for having the wrong nonce until transaction 2 executes.


#### Details of `rlp_execute`

This function is named after the **RLP** standard in Ethereum. In particular, the `tx_bytes_b64` argument is be parsed into bytes from base-64 then those bytes are parsed into structured data assuming it is RLP encoded. The structure the data is parsed into is 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:
birchmd marked this conversation as resolved.
Show resolved Hide resolved

- `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**

Check failure on line 80 in neps/nep-0518.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Emphasis used instead of a heading [Context: "Ethereum transaction validatio..."]

neps/nep-0518.md:80 MD036/no-emphasis-as-heading/no-emphasis-as-header Emphasis used instead of a heading [Context: "Ethereum transaction validatio..."]

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**

Check failure on line 90 in neps/nep-0518.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Emphasis used instead of a heading [Context: "Converting Ethereum transactio..."]

neps/nep-0518.md:90 MD036/no-emphasis-as-heading/no-emphasis-as-header Emphasis used instead of a heading [Context: "Converting Ethereum transactio..."]

TODO

#### Interaction with Web3 relayers

TODO

## Reference implementation

The protocol changes necessary for the 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.
- **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)

TODO

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
Loading