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

Native EVM #106

Closed
wants to merge 2 commits into from
Closed

Native EVM #106

wants to merge 2 commits into from

Conversation

ilblackdragon
Copy link
Member

@ilblackdragon ilblackdragon commented Aug 14, 2020

Motivation

There are few reasons to support EVM as an alternative virtual machine:

  • Large number of already existing and audited code in financial and blockchain space
  • EVM have been tested in production with large amounts of money at stake, which also built up a knowledge of common issues while people building contracts.
  • EVM as an environment provides sync transactions which is unlocks some of the sets of applications that are not really hard or impossible to compose otherwise.

Pre-compile vs contract:

  • We have an option of a WASM contract https://github.com/near/near-evm but the virtualization via WASM has so far shown poor performance.
  • There are few critical pieces of integration with data that will dramatically improve performance and usage of EVM via a pre-compile (data layout, etc)

Details

TBD

Stability plan

This should list the plan of tests that ensure stability of this change.

Protocol Upgrade

The protocol upgrade will:

  • Rollout support for pre-complies in general
  • All accounts that have namespace ".evm", including top level "evm" will be EVM contracts.
  • Any state changes due to difference of representing data between https://github.com/near/near-evm and implementation should remap the data in the data migration.

@render
Copy link

render bot commented Aug 14, 2020

@evgenykuzyakov
Copy link
Contributor

Overall, it's a bad idea to have precompile as is:

  • (fixable) It has a bunch of bugs.
  • (fixable) Overhead of collections
  • (doable) No audit of the contract itself (only EVM crates).
  • (fixable) Doesn't use EVM gas and relies on Wasm gas metering.
  • Interface is not ideal, but works through existing actions. It's still a protocol change to introduce it, so we can introduce new actions as well for EVM specific.

account_id = "evm" with code_hash = "0x1"

  • It's a hack and really bad one by representing code hash of non existing contract.
  • That's not scalable at all. We need to have multiple accounts with their EVM contracts.
  • Legacy support and upgrades are painful.

@abacabadabacaba
Copy link
Collaborator

@ilblackdragon Did you consider transpiling EVM bytecode to WASM? This should provide significantly better performance than having an EVM interpreter in WASM.

An even better solution is to compile Solidity directly to WASM. This will allow more efficient arithmetic for types smaller than 256 bits, more efficient storage layout, and possibly other optimizations. But this is more difficult than transpiling EVM bytecode.

How do you plan to implement sync transactions on a sharded blockchain?

@ilblackdragon
Copy link
Member Author

There are two problems with what you are suggesting @abacabadabacaba:

  • There is no existing tools for this and all projects that have been trying to do this are very far from production.
  • Our runtime is too different, there needs to be mapping somewhere. Doing mapping in compiler or transpiler sounds way to complex
  • Sync transactions will be inside EVM environment.

RE: @evgenykuzyakov I'm not suggesting we literally dump near-evm into nearcore.
I'm suggesting we adjust to use internal APIs for storage. Use EVM's gas and remap it into our gas amounts.

That's not scalable at all. We need to have multiple accounts with their EVM contracts.

This approach allows to have as many as one wants of EVM contracts. We just need a way to create accounts with this hash.

upgrades are painful

Updates are easier than currently. If near-evm as a contract has issues, we don't really have a way to upgrade it. But if it's pre-compiled we can fix things inside nearcore and release a new version.

It has a bunch of bugs.

For example?

It's a hack and really bad one by representing code hash of non existing contract.

What would be a different way? This is similar how Ethereum does it, but I'm open for alternative.

@evgenykuzyakov
Copy link
Contributor

Example of a bug: near/near-evm#46

@abacabadabacaba
Copy link
Collaborator

@ilblackdragon What are the use cases of such a feature? If we only have isolated "EVM environments" which behave like an Ethereum instance, this doesn't seem very useful.

In general, while it seems desirable to able to make Ethereum contracts usable on NEAR, there is probably no good way to do it. The main issue is the lack of synchronous calls (there are other issues, such as differences in addressing). While there are ways to work around this, they all have serious problems:

  • Allowing synchronous calls within isolated "EVM environments" will greatly reduce the usability, because in this case interaction with everything outside that "environment" will require some sort of "bridge" which will not be able to fully mimic Ethereum semantics anyway.
  • Turning synchronous calls into asynchronous ones is even more problematic. Reverts will be impossible, and there are other subtle differences.

Both approaches may introduce security vulnerabilities into contracts. So, a contract that is secure on Ethereum may be vulnerable when run on NEAR. Trying to make as many contracts as possible run on NEAR without any changes will encourage developers to run their existing contracts on NEAR without performing a separate security review, which may have devastating consequences.

Also, I am very much against introducing "precompiled contracts" into NEAR. Any functionality built into the node should be exposed as host functions.

I am also against building Ethereum into NEAR. Every time there is an Ethereum hard fork, would we need to hard fork NEAR as well?

@ilblackdragon
Copy link
Member Author

ilblackdragon commented Aug 27, 2020

Btw @evgenykuzyakov a reason to make EVM precompile vs having a custom tx actions is to later be able to yank it back out into a smart contract land with a simple protocol upgrade.

@abacabadabacaba we have dozens of partners who would launch immediately in am EVM environment like described in this proposal (we are currently testing the experience and everything with a smart contract based EVM).

To be clear, this is not Ethereum on NEAR. This is EVM environment on NEAR.
Same as it is right now with https://github.com/near/near-evm/. This proposal just makes it more performant while we can actually speed up the contracts.

@SkidanovAlex
Copy link
Contributor

@abacabadabacaba specifically of the two options you outlined, the current approach is the first: the contracts within the environment have full interop, and if you want to "escape" the environment, you use some sort of a bridge.

@ilblackdragon ilblackdragon mentioned this pull request Aug 28, 2020
12 tasks
@ilblackdragon
Copy link
Member Author

One concern to think through is the storage costs.
Usually in Ethereum it's not expected to cover the storage with the balance on the contract.
On the other hand, current code already will revert all changes if the result will exceed allowed space with LackOfBalance error.

@MaksymZavershynskyi MaksymZavershynskyi changed the title EVM pre-compile Native EVM Aug 29, 2020
@abacabadabacaba
Copy link
Collaborator

Currently, I see three major problems with this proposal:

  1. Interoperability: EVM contracts will not be able to natively interact with NEAR contracts, and vice versa. While there is some support for the NEAR token, many EVM contracts are designed to work with other tokens (usually ERC-20), and I can't find any details on how those are going to be bridged to NEAR.

  2. Scalability: even if there will be multiple "EVM environments", most developers are likely to deploy everything to a single such environment (which is the same for everyone). The reason is better interoperability with other EVM contracts. Also, the solution for bridging ERC-20 tokens will probably require a separate EVM contract deployed for every such token in every EVM environment, so the developers will prefer to deploy their EVM contracts into an environment that already includes all the tokens they may want to use.

  3. Security: there will necessarily be some differences between EVM on NEAR and EVM on Ethereum. These differences may result in contracts being secure when used on Ethereum blockchain but not on NEAR blockchain. Interaction between NEAR native contracts and EVM contracts is another place where security issues may occur. Also, adding a whole new runtime may negatively affect the security of NEAR itself.

@MaksymZavershynskyi
Copy link
Contributor

Async-Sync Interoperability

  • Interoperability between sync and async code is already solved in regular programming languages with callbacks;
  • EVM contracts can refer to NEAR addresses the way people dial on the phone "the outside line" when calling from the company -- we can use a special EVM address (e.g. 0xFF...F) for the outside line to call all NEAR contracts.

Here is how it would work:

Calling NEAR contract from EVM contract

Suppose EVM contract wants to call NEAR contract on near.dex01 on method near.method01 with args near.args01.

import "../NearContract.sol";

contract MyEVMContract {
   function callNearContract() public {
      NearContract contract = NearContract.from("near.dex01");
      contract.call("near.method01", "near.args01");
   }
}

This will schedule an asynchronous call to the NEAR contract that will be done asynchronously after EVM contract is finished.

Calling NEAR contract from EVM contract with callback

Suppose we want the same EVM contract (or other EVM contract) to be able to receive the callback from NEAR contract when it is done.

import "../NearContract.sol";

contract MyEVMContract {
   function callNearContract() public {
      NearContract contract = NearContract.from("near.dex01");
      contract.callWithEVMCallback("near.method01", "near.args01", address(this), "evmMethod02", "evm.args02");
   }

  function evmMethod02(bytes memory args, bytes memory callbackArgs) public {
  }
}

Similarly we can have contract.callWithNEARCallback.

Calling EVM contract from NEAR contract

NEAR contract call API will look exactly the same, except it would require providing address of EVM and address inside EVM.

Regular low-level Rust API:

env::promise_create("near_contract", b"set_status", b"args_blob", balance, gas);

EVM low-level Rust API:

env::promise_create("evm_address", "evm_contract_address", b"set_status", b"args_blob", gas);

Regular high-level Rust API:

ext_status_message::set_status(message, &"near_contract", balance, gas);

EVM high-level Rust API:

ext_status_message::set_status(message, &"evm_address", &"evm_contract_address", gas);

Callbacks and other features of our promises will work out of the box.

Multiple EVMs

We can easily allow unlimited number of EVMs, so that once we have sharding these EVMs can even work with each other using the above described API. This will make NEAR and actual Ethereum 2.0!

Specifically we hardcode that contract bytes 0x000001 correspond to EVM (in the future we can allow 0x000002, 0x000003, etc correspond to a RISCV VM, Libra VM, Ewasm VM, alternative implementations of EVM, etc). So anyone can deploy as many instances of EVM as they want on their accounts.

Runtime execution example

Suppose some NEAR contract does the following call:

ext_status_message::set_status(message, &"evm_address", &"evm_contract_address", gas);
  • The actual receipt will have receiver="evm_address" and args will be packed as borsh(evm_contract_address, abiEncoded([message])) (pseudocode).
  • When transaction runtime executes this receipt it loads the bytes deployed to evm_address. If they are 0x000001 then it does not run Wasm and instead unpacks evm_contract_address and abiEncoded([message]) and passes it to EVM.

EVM calling another EVM with callback

Suppose we want to call one EVM from another EVM (potentially on different shards) and even get a callback!

Contract on the first EVM:

import "../ShardedEVM.sol";

contract Contract1 {
   function doSomething() public {
      ShardedEVM other = ShardedEVM.from("another_evm_address");
      other.callWithEVMCallback("evmMethod01", "evm.args01", address(this), "callbackMethod", "evm.args02");
   }

  function callbackMethod(bytes memory args, bytes memory callbackArgs) public {
  }
}

Contract on the second EVM:

contract Contract2 {
   function evmMethod01(bytes memory args) public {
   }
}

Potentially, we don't even need to operate with untyped args bytes memory args and use ABI encoding similarly to how we wrap use borsh. (I haven't fully thought it through)

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented Sep 3, 2020

@abacabadabacaba

EVM contracts will not be able to natively interact with NEAR contracts, and vice versa.

While EVM contracts will not have good standards for interacting with other EVMs or NEAR contracts, it is fully possible, just dangerous, and people should not be using complex interops for DeFi applications. Only simple interops, like burn this ERC20 token and mint some NEP21 token, we have already established an even more complex interop with Rainbow bridge.

. Also, the solution for bridging ERC-20 tokens will probably require a separate EVM contract deployed for every such token in every EVM environment, so the developers will prefer to deploy their EVM contracts into an environment that already includes all the tokens they may want to use.

This logic shouldn't be hard to hide behind near-sdk-rs and some Solidity library. Additional information in the receipts (like who is sender from within EVM) should be sufficient for establish safe links between sharded ERC20 contracts or between ERC20 and NEP21.

One concern to think through is the storage costs.
there will necessarily be some differences between EVM on NEAR and EVM on Ethereum.

@ilblackdragon and @abacabadabacaba

The goal is to not have any differences but gas price and the above mentioned 0xFF...F address. We can actually map ETH gas to NEAR gas in such way that ETH developers don't need to know that we use gas to purchase tokens for state staking inside EVM.

In Ethereum allocation of one key pair (32 bytes, 32 bytes) costs 20K ETH gas.
In NEAR it costs 10298778625+21301916*32+ 9153361*32=11G NEAR gas and 0.0058 NEAR staking.

Suppose we map 1 ETH gas = XM NEAR gas, then writing (32 bytes, 32 bytes) inside EVM will cost X*20G NEAR gas, where 11G NEAR gas will be spent on storage operation, and remaining X*20G - 11G NEAR gas can be spent on state staking. For this to work the following should be true: X*9*10^9 * <minimal gas price> >= 0.0058*10^24, since minimal gas price is 1000000000, X >= 644.

So if we map 1 ETH gas >= 644M NEAR gas we don't need to worry about staking. Typical Ethereum contract calls require 100-200k ETH gas, which will cost 6.44e+13 NEAR gas, or 0.0644 NEAR at the base NEAR gas price, which is still much cheaper than ETH contracts. So this scheme is technically feasible.

@bowenwang1996
Copy link
Collaborator

So if we map 1 ETH gas >= 644M NEAR gas we don't need to worry about staking. Typical Ethereum contract calls require 100-200k ETH gas, which will cost 6.44e+13 NEAR gas, or 0.0644 NEAR at the base NEAR gas price, which is still much cheaper than ETH contracts. So this scheme is technically feasible.

If we do this, based on our current limit ethereum contract calls that need more than ~300k eth gas will not be able to fit. Do we want to have separate limiters for evm calls?

@MaksymZavershynskyi
Copy link
Contributor

So if we map 1 ETH gas >= 644M NEAR gas we don't need to worry about staking. Typical Ethereum contract calls require 100-200k ETH gas, which will cost 6.44e+13 NEAR gas, or 0.0644 NEAR at the base NEAR gas price, which is still much cheaper than ETH contracts. So this scheme is technically feasible.

If we do this, based on our current limit ethereum contract calls that need more than ~300k eth gas will not be able to fit. Do we want to have separate limiters for evm calls?

Good point. Some exchanges use 2M ETH gas in a single call, so we won't be able to solve it by increasing the limit. It also means we cannot map 1 ETH gas = 644M NEAR because the exchanges might be using a lot of gas on pure compute. We can reduce X, but then when ETH contract allocates more storage we first try to get tokens from staking from the attached tokens, and only if no tokens are attached will be purchasing them on the spot using the gas. WDYT @evgenykuzyakov ?

@abacabadabacaba
Copy link
Collaborator

All of this will require writing EVM smart contracts specifically for NEAR. Wasn't the point of NEAR to switch from unfamiliar languages like Solidity to more efficient and well-known languages like Rust and TypeScript? And even if we want to have Solidity as one of supported languages, isn't it better to compile it to WASM instead of having a wholly separate execution environment?

Even Ethereum devs want to switch from EVM to WASM in Ethereum 2.0. They can't do it in Ethereum 1.0 because of smart contracts that are already there. We are going in the opposite direction, adding an inferior execution environment which we will have to maintain for years instead of building something better.

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented Sep 4, 2020

@abacabadabacaba

All of this will require writing EVM smart contracts specifically for NEAR. Wasn't the point of NEAR to switch from unfamiliar languages like Solidity to more efficient and well-known languages like Rust and TypeScript?

We need to take into account that there is already an ecosystem that exists around EVM/Solidity, which includes contract writing patterns that are not easy to move away from. It is similar to other existing ecosystems, e.g. many web-devs want to move from JS to other well-known languages, e.g. TypeScript and maybe some of the browsers would want to do it too by introducing Wasm, but they still have to support JS because of the numerous JS libraries, frameworks, and ecosystem as a whole. NEAR is analogous to browsers, and Solidity is analogous to JS.

Also, we are attempting two difficult transitions here: from Solidity to Wasm, and from synchronous programs to asynchronous programs. It is very difficult to do both at the same time, e.g. I remember how much of the push back there was when multicore CPUs started appearing and an average developer was super skeptical about learning parallel programming patterns. So if we bank on being able to transition the existing Ethereum ecosystem in both directions simultaneously we might not succeed in transitioning at all.

And even if we want to have Solidity as one of supported languages, isn't it better to compile it to WASM instead of having a wholly separate execution environment?

Writing a compiler of even a relatively simple language to Wasm is not an easy task and would require a separate team. There are many languages that claim to have Wasm support, but realistically we can only consider Rust->Wasm production ready. (and maybe with huge question mark C++->Wasm or AS->Wasm)

Also, it is not just about compilation of one language into another language. We want to have 100% spec-compatible EVM support, which means we need to mimic gas counting, and host functions. If we are going to pollute our Wasm runtime with EVM-specific host functions, have EVM-specific gas metering (Wasm gas metering is protocol-level thing) then we are already introducing a new "runtime" and there is little added benefit of running the Solidity code itself with Wasm rather than EVM interpreter.

Even Ethereum devs want to switch from EVM to WASM in Ethereum 2.0.

Whether they are going to switch and how exactly it will look like is very uncertain.

@abacabadabacaba
Copy link
Collaborator

NEAR is analogous to browsers, and Solidity is analogous to JS.

Today's browsers are horrible. Of monstrous complexity, full of kludges, with new vulnerabilities being found all the time. And only two companies in the world are capable of developing a modern browser.

I very much hope that NEAR will never be like a browser.

We want to have 100% spec-compatible EVM support, which means we need to mimic gas counting, and host functions.

There is a problem: the EVM Ethereum is using now is not 100% compatible with the EVM Ethereum used a year ago. During that period, it gained two new opcodes and one new precompile, and multiple other opcodes had their gas usage changed. Does it make sense to keep 100% in sync with such a moving target? I think that most contracts don't care how the gas is measured at all, as long as they don't run out of it.

@MaksymZavershynskyi
Copy link
Contributor

I think that most contracts don't care how the gas is measured at all, as long as they don't run out of it.

This is actually the reason why they really care about gas measurement. AFAIK in the past Ethereum has increased some op cost and it broke a bunch of contracts because they relied on specific costs. Many DeFi contracts are notoriously optimized in their gas usage, they optimize to reduce the usage of specific op costs and try to not have loops as much as possible. So if we don't have exact gas metering we will risk some random DeFi contracts not working exactly. Also, it is also a very strong message to say that NEAR EVM is exactly the same as Ethereum EVM.

@abacabadabacaba
Copy link
Collaborator

AFAIK in the past Ethereum has increased some op cost and it broke a bunch of contracts because they relied on specific costs.

For this reason, relying on specific costs is discouraged nowadays.

Many DeFi contracts are notoriously optimized in their gas usage, they optimize to reduce the usage of specific op costs and try to not have loops as much as possible.

That's because on Ethereum gas is expensive. They do it to reduce costs, not because changing gas metering will break their contracts. If we count gas differently but provide enough of it to make these contracts work, they will work. Eventually, if the contract authors will want to optimize gas usage on NEAR, they will have to use WASM because it is more efficient.

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented Sep 4, 2020 via email

@ilblackdragon
Copy link
Member Author

ilblackdragon commented Sep 11, 2020

EcRecover handling

Context: there is a pattern of meta-transactions on Ethereum that leverages next flow:

  • User signs a message '\x19Ethereum message:\n' || len(bytes) || bytes -- this is supported by all the major wallets
  • User sends such message and signature to a relayer
  • Relayer usually is some operator (exchange operator) or just a "gas station network"
  • Relayer takes this messages, creates Ethereum transaction and send it over to Ethereum network
  • The receiving contract then verifies the message and uses ecrecover from signature to determine the public_key that signed it. From public_key the address is inferred and this address used as initiator of the action.
  • In case of exchanges, this is usually sent to the exchange contract. Such contract would settle the trades on behalf of inferred address or withdraw funds to their address.
  • In case of more general gas station, it would extract a method/args and call a contract on behalf of the inferred address. This requires modifying the receiving contract to use custom method for msg.sender.

How will NEAR EVM handle this:

  • This will be only supported for secp256k1 key pairs. E.g. if message is signed with ed25519, the ecrecover will fail.
  • We are adding support for Ethereum addresses, e.g. secp256k1's keccak256(public_key)[12..] is a valid address inside NEAR's EVM.
  • These addresses will be accessible/used if NEAR transaction signer_pk is secp256k1 (e.g. in this cases signer_id will be ignored)
  • We add an option to send meta-transaction directly to EVM "contract". E.g. a format of message '\x19Ethereum message:\n' || len || contract_address || method_name || args, which calls a contract address on behalf of the user (e.g. msg.sender == ecrecover(signature)`.

Some Cases that this handles

  • Frontend with key management signs on user's behalf and sends to operator. Operator signs tx and sends to the contract. Contract ercrecover(signature) to determine address of the user to attach action to.
  • User signs a message in Metamask and sends it to the app. App's backend relays it to the NEAR network via EVM and action gets executed on the behalf of this user. Financially, this should include that action also pays to the relayer/app something (but can be something like deposit to exchange where exchange would get money in some other way).
  • User signs a message in Metamask and sends it to exchange backend. Exchange backend relays it to an exchange contract in NEAR EVM. Exchange contract ecrecover(signature) and depending on the message makes an action on behalf of the user.

Notes

  • If ecdsa key is used as a limited function call access key, this should be working fine - because key should be targeted at evm account in the first place to allow create a receipt that will execute.

@ilblackdragon
Copy link
Member Author

Also good question if we want to restrict creation of EVM accounts.
E.g. if we define anything under ".evm" to be an EVM account, should we limit creation of such accounts?

Current suggestion to add a "registrar" method to EVM, that will create sub-account if account_id == "evm":

   fn create_account(&self, name: String) -> Result<()> { 
      if self.account_id != "evm" return EvmError::AccountCreationOnlyTopLevel();
      Promise::new(format!("{}.evm", name)).create_account().transfer(self.attached_deposit);
   }

@prestwich
Copy link

public_key[..20]

nit: this should be keccak256(public_key)[12..]

@ilblackdragon
Copy link
Member Author

Open question: how to allow Ethereum meta transactions to propagate to WASM contracts.

WASM contracts will require account_ids: signer_id and predecessor_id.
But we don't have ecdsa implicit accounts, which means that can not infer one from the key (CC @evgenykuzyakov).
Also rewriting signer_id is dangerous, but can rewrite predecessor_id if we can infer an account somehow.

Use case:

  • Use signs a meta-tx with Metamask to call sometoken to transfer some funds out.
  • It gets relayed to evm contract meta_tx method.
  • This creates a promise to sometoken.transfer where predeccessor is some account of the user that signed transaction.

@ilblackdragon
Copy link
Member Author

Meta transaction message format

Requirements:

  • different chains (testnets, forks) should differ on the chainId. Need to add chain id to https://github.com/ethereum-lists/chains
  • update to the version of the EVM internal protocol should invalidate the signature
  • keep track of the nonce of the signer to prevent replays
  • message differs for different EVMs

Data format complaint with EIP-712:

  • \x19\x01 - prefix for EIP-712
  • domainSeparator:
    keccak256(abi.encode(
    keccak256("EIP712Domain(string name,string version,uint256 chainId)"),
    keccak256(bytes("NEAR")),
    keccak256(bytes(1)),
    chainId, // how about 1313161554? hex of NEAR?
    ));
  • keccak256(abi.encode(
    keccak256("NearTx(string evmId, uint256 nonce, address contractAddress, bytes arguments)",
    bytes(""), // NEAR account id of the EVM
    bytes(nonce), // nonce of the given account
    bytes(contractAddress) // address inside this EVM
    arguments
    ));

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented Sep 24, 2020

User signs a message '\x19Ethereum message:\n' || len(bytes) || bytes -- this is supported by all the major wallets

@ilblackdragon I think there is a typo. It should be:

"\x19Ethereum Signed Message:\n" || len(bytes) || bytes. See https://ethereum.stackexchange.com/a/15911

@ilblackdragon
Copy link
Member Author

ilblackdragon commented Sep 30, 2020

Meta Transaction payments:

We add an extra (address feeTokenAddress, uint256 feeAmount) parameter to meta message.
This parameter tells protocol to charge this amount of given token from the signer of the message's account.
Relayer decides if they want to relay such transaction based on the token address and amount -- e.g. if they want to accept payment in DAI or not.

fn meta_call(&mut self, args: Vec<u8>) {
   let { signature, fee_token_address, fee_amount, contract_id, args } = parse_args(args);
   sender = ecrecovery(hash(prepare_message(fee_token_id, fee_amount, contract_id, args)), signature);
   
   // calling ERC20(payment_token_id).transfer(relayer_id, amount) on behalf of the message signer.
   let {result, _} = interpreter::call(sender, fee_token_address, erc20_abi::encode::transfer(relayer_id, fee_amount));
   if result == failure -> abort

   let {result, gas} = interpreter::call(sender, contract_id, args);
   let relayer_id = near_account_to_evm(self.predecessor_id);
   result
}

Flow:

  • User indicates to frontend what action they want to execute and what token to pay with
  • Frontend queries relayers to get a "quote" on payment. Relayers can decide for themself on amount and if they want to support requested tokens
  • User signs a transaction that shows some amount of some token will be charged (or 0x0 if it’s free)
  • Any relayer can check again if they want to accept such message with given payment token/amount
  • Protocol executed the message and charges user, by transferring to the predeccessor_id's address given amount.

DApp specific relayers can decide to not charge anything, and accept fee_token_address == 0x0, which will pretty much waive the payment.

Question:

Should user (frontend) specify the amount or should amount be inferred from the gas cost + gas amount inside EVM + some mark up to cover for external gas?

Pros for specifying amount, is that relayer can charge whatever they want. User can decide to sign or not based on the cost.
But this may lead to cost of relayer growing is the cost of the gas increased.

Alternatively protocol (near_evm_runner::EvmContext::meta_call) knows the current gas costs that are paid and can charge the exact amount that was spent. Relayer paid extra fee.

Concerns

Relayer grieving

  • user signs a message that will pay 1 DAI to relayer to execute tx
  • relayer checks that user has 1 DAI
  • user transfers out 1 DAI away from account
    relayer sends the tx paying $NEAR for fee but tx fails because there is no DAI on users account

I don't think it's a big issue, as user doesn't win anything and 1s blocks prevent doing this really easily as relayer may send their tx faster most of the times.

@alexauroradev
Copy link

I believe proposed ability to natively charge a user in ERC20 (NEP21?) is a good addition. It covers two cases: when the relay is disconnected from the user and needs to be payed; and when it is connected to the user being, e.g., a native relay for a dApp, and the relay incentive is lying outside the blockchain economics (e.g. withdraw fees on exchanges). Proposed approach covers the both cases.

I think it's better to specify the relay fee amount. Since we do have a gas cost in $NEAR, but probably don't have it in NEP21.

Supporting the payment double spend concern.

@ilblackdragon
Copy link
Member Author

NEP-21 would break atomicity of this action, and also would require to have allowance on the token to do so (because it will be executed out of the context).

With ERC-20, because we are controlling EVM runtime from the invocation - we can just call transfer on the ERC-20 token directly with sender been the recovered signatory. E.g. this doesn't require extra allowance on the relayer or anything like that.

@alexauroradev
Copy link

Ok, got it with the atomicity. Would note here only that this is all about ERC20 in NEAR EVM.
However, the problem with gas price nominated in NEAR EVM ERC20 token still remains.

@alexauroradev
Copy link

@ilblackdragon , there's a suggestion on the meta-transaction fees.

There might be many tokens a user wants to trade, and the nomination of fees in these tokens can be complicated. However, all Ethereum users are used to pay for the transactions in Ether.

Why don't we introduce nETH in NEAR (this can be done through the Bridge)? A user will have some nETH in NEAR EVM and we the relayer will charge user in nETH for the meta_calls. The missing link here is how to compute the price for NEAR Gas nominated in nETH. We know how to calculate nearGas(evmGas), but we need to know the exchange rate between nETH and $NEAR. This can be solved in two ways:

First option, we introduce in NEAR an oracle that commits ETH <> $NEAR weighted exchange rate.
Second option (proposed by @ailisp ), is to introduce a Uniswap-like decentralised exchange contract that will cover both nETH and any other ERC-20. But it would need to have enough liquidity to display correct rates.

Thoughts?

@ilblackdragon
Copy link
Member Author

ilblackdragon commented Oct 5, 2020

Your suggestion is too complicated as oracles on-chain and especially inside the protocol would be very complicated to implement correctly and I think is limiting in functionality, as even in Ethereum many people don't have ether but it's even less likely when they migrate to NEAR to use apps (as moving each asset is extra cost).

A much simpler solution is to extend the previous idea and allow relayer to define price for whatever currency user wants and user sign on it.

This way relayer calculates their costs, can use whatever price for the token user have and then quote it to user.
User just signs meta transaction with that encoded if they agree to pay it.

E.g. this will support charging user nETH, DAI or anything else.

So flow between frontend and relayer can be:

  1. user initiates action and selects which token they want to pay with
  2. frontend prepares transaction and send for quote to relayer
  3. relayer calculates the cost and returns back to the frontend
  4. frontend presents this to user and user signs it via MetaMask
  5. send this to relayer, relayer verifies that meta transaction make sense and they are willing to pay for it and relays.

Initially, it can be that relayer just supports nETH and can extend the relayer interface later with quoting mechanism.

evgenykuzyakov pushed a commit to near/nearcore that referenced this pull request Dec 3, 2020
Implements near/NEPs#106

- [x] PR for Ethereum chain_id
- [x] Return deterministic errors
- [x] evm accounts are EVM.
- [x] Create sub-account from evm are removed and we don't plan to support it originally.
- [x] Integrate gas usage #3299 
- [x] Meta transaction calls are currently disabled
- <s>Precompile in EVM to call outside to NEAR</s> Moved to separate issue
- [x] Protocol version upgrade to 42 from which `evm` accounts activate.
- [ ] Add a check that the `evm` account doesn't have access keys.

Reviews
----
- [ ] @bowenwang1996 please review changes around genesis/protocol, so we don't leak this feature accidentally.
- [ ] @nearmax if you want to take a second look at runtime changes.
- [ ] @frol do we need to do something from the RPC/indexer perspective?
- [ ] @ilblackdragon In case you want to take a look as well.

Test plan
----

* Set of Solidity contracts that can be run inside EVM (see runtime/near-evm-runner/tests/contracts)
* Interact with CryptoZombies contract via runtime & RPC test
* <s>Nightly test that will leverage full e2e tooling to deploy & test a full fledged contract(s)</s> Moved to separate issue
* http://nayduck.eastus.cloudapp.azure.com:3000/#/run/764
* To the moon
@ilblackdragon
Copy link
Member Author

Alternative design that leaves the interface the same but cleanly separates the performance critical pieces:

  • Host function run_evm(bytecode_key, input), where bytecode_key is a key in storage for bytecode; input is ABI encoded input for the call.
  • Set of host functions to work with secp256k1 crypto, including ecrecover.
  • Contract near-evm that has all the glue code for executing contracts. This will contain implementation of $ETH as base token, connectivity to the bridge, all the logic for meta tx, tx relay and other required logic.

Pros:

  • Removes tons of code from nearcore
  • Keeps all the logic on the contract level, making it way easier to test and iterate on
  • Defines JSON API via smart contract vs protocol (protocol level JSON would require over specification of JSON parser details, as JSON parsers differ dramatically between each other)
  • Leaves upgradability in contract level via Smart Contract Upgradability #123 pattern vs requiring full protocol upgrade.
  • Anyone can build and deploy their own EVM, where they fork the contract and still use the same precompiled execution engine.

Cons:

  • Some performance hit, due to moving some of the logic to WASM. Can be mitigated by having set of whitelisted contract hashes that are compiled with optimized (LLVM) compiler

An open for discussion topic is around covering storage. See https://gov.near.org/t/storage-staking-price/399 more discussion on storage staking in general. Given we haven't had a clear decision before, this actually opens up opportunity to say that transactions to EVM should cover storage increases in it via attached amount. Reducing price of storage might be important piece here to make things indeed cheap.

@ilblackdragon
Copy link
Member Author

ilblackdragon commented Jan 29, 2021

Quick update on the experiments around separating part of EVM glue code back into a smart contract:

  • The interpreter part is tightly coupled with "Ext" - extension that provides all of the interaction with state (accounts and contracts).
  • Which means that to keep interpreter "precompiled", still need to keep NearExt with specific implementation of the state accessors. And these would need to be duplicated inside the EVM contract
  • I have this in a branch, but I don't really like this design.

I've brushed the dust off the EVM contract (https://github.com/near/near-evm).
Tried to see what if we keep the whole EVM runtime in the contract and just optimize it's usage (e.g. remove secp library).
It's about ~100x slower with singlepath compiler vs the native compiled.
Trying to compile with LLVM.

@ilblackdragon
Copy link
Member Author

Closing because of Aurora.

@bowenwang1996 bowenwang1996 deleted the evm-precompile branch November 25, 2021 01:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants