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

EVM support #225

Merged
merged 58 commits into from
May 22, 2024
Merged
Changes from 9 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7f73d24
evm support flip
ramtinms Nov 27, 2023
1a8a6ec
fix typos
ramtinms Nov 28, 2023
0c99501
Apply suggestions from code review
ramtinms Dec 1, 2023
3a3a805
Apply suggestions from code review
ramtinms Dec 1, 2023
fbb565c
Apply suggestions from code review
ramtinms Dec 1, 2023
4db769c
apply pr feedbacks
ramtinms Dec 4, 2023
b8b0378
set issue number
ramtinms Dec 4, 2023
d82c91d
Apply @turbolent's suggestions from his review
franklywatson Dec 11, 2023
c0e3a0f
apply PR feedback
ramtinms Dec 13, 2023
5dad2cb
Update protocol/20231116-evm-support.md
franklywatson Dec 13, 2023
8323c2b
Reference uniqueness of BridgedAccount
franklywatson Dec 13, 2023
5c3004e
Link to more tech information about Flow consensus
franklywatson Dec 13, 2023
f646491
Apply suggestions from code review
turbolent Dec 20, 2023
dac4fe4
Added image directory and EVM diagrams
franklywatson Dec 22, 2023
7cd8dc6
Updated FLIP to reflect new naming
franklywatson Dec 22, 2023
900af71
Added Flow EVM account model diagram
franklywatson Dec 22, 2023
bcd7a14
Further tidyup
franklywatson Dec 22, 2023
27c4675
Restore diagram from merge error
franklywatson Dec 22, 2023
d2176fc
add language to code fences
turbolent Jan 5, 2024
6dbd64d
document EVM.run
turbolent Jan 5, 2024
eb3543d
improve API
turbolent Jan 5, 2024
aa6ca62
convert example script to transaction
turbolent Jan 5, 2024
586aa0f
update flip with recent changes
ramtinms Jan 15, 2024
3411fbe
typos
ramtinms Jan 15, 2024
bf0dcf2
typos
ramtinms Jan 15, 2024
5e65fa1
Apply suggestions from code review
turbolent Jan 17, 2024
550a270
link to ERC-721
turbolent Jan 17, 2024
69f77cd
Add line break
franklywatson Jan 17, 2024
631055f
small improvements
ramtinms Jan 18, 2024
cc76ab1
change state toproposed
ramtinms Jan 18, 2024
683be9f
update isValid draft
ramtinms Jan 18, 2024
318b15b
link to ERC-1271
turbolent Jan 18, 2024
93931f4
fix deposite
ramtinms Jan 24, 2024
5c78cb6
Minor naming change
franklywatson Feb 5, 2024
ca025f7
Update protocol/20231116-evm-support.md
ramtinms Feb 21, 2024
3dc0125
Update EVM support FLIP with new interface & Cadence 1.0 refactor (#249)
sisyphusSmiling Feb 23, 2024
3e6c3b8
update evm contract
ramtinms Mar 4, 2024
f9ae903
add status codes
ramtinms Mar 4, 2024
2023b5a
update doc about run
ramtinms Mar 4, 2024
77068ea
add any evm address deposit to the contract
ramtinms Mar 5, 2024
d010243
add details about the token bridge
ramtinms Mar 11, 2024
e0122e0
Update deploy return type
sideninja Apr 2, 2024
3f8c071
update Cadence 1.0 syntax
sideninja Apr 2, 2024
eb6f104
update deploy return result type
sideninja Apr 11, 2024
45ae318
update email
turbolent Apr 11, 2024
def9a09
deploy update comment
sideninja Apr 11, 2024
21c817e
update deploy result optional
sideninja Apr 23, 2024
13cc05f
Update protocol/20231116-evm-support.md status
franklywatson Apr 24, 2024
a0e9aac
Add batch run to EVM FLIP (#257)
sideninja Apr 29, 2024
eff8cb2
updates to all the changes
sideninja Apr 29, 2024
29c4b4a
add dry run
sideninja Apr 29, 2024
6c185ab
update with events
sideninja Apr 29, 2024
d717807
add missing comma
sideninja Apr 29, 2024
bacc4cc
revert status change, FLIP is not approved yet
turbolent May 7, 2024
380fb8f
update with hex-encoded events and revertible random
sideninja May 20, 2024
39a4bb9
put back appendix c
sideninja May 20, 2024
a1793b2
make functions view
sideninja May 20, 2024
70e1ae2
update view funcs
sideninja May 20, 2024
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
192 changes: 192 additions & 0 deletions protocol/20231116-evm-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
---
status: draft
flip: 223
authors: Ramtin Seraj ([email protected]), Bastian Müller ([email protected])
turbolent marked this conversation as resolved.
Show resolved Hide resolved
sponsor: Dieter Shirley ([email protected])
updated: 2023-12-04
---

# FLIP 223: EVM integration interface

## Objective

- Defining a Cadence interface for the EVM integrated into the FVM.
- Facilitates seamless interaction between Cadence and EVM environments.

## Motivation

Following the discussion [here](https://forum.flow.com/t/evm-on-flow-beyond-solidity/5260), this proposal outlines a path to achieve full EVM equivalence on Flow, enabling developers to deploy any Ethereum decentralized application (dApp) on Flow without making any code changes. This allows developers to fully leverage the native functionality of Flow. Trusted tools and protocols such as Uniswap, Opensea, Metamask, Chainlink Oracle, Layerzero, AAVE, Curve, Remix, and Hardhat will be readily compatible with Flow. Additionally, developers will still have the ability to write Cadence smart contracts to extend and enhance the functionality of Solidity smart contracts, ensuring full composability.

Support for EVM on Flow enables developers to leverage the network effects and tooling available in Solidity and EVM, while also benefiting from Flow's user-friendly features and mainstream focus for onboarding and user experience. Additionally, developers can take advantage of Cadence's unique capabilities.

## Design Proposal

#### EVM as a standard smart contract

To better understand the approach proposed in this Flip, consider Flow EVM as a virtual blockchain deployed to the Flow blockchain at a specific address (e.g., a service account). EVM on Flow functions as a smart contract that emulates the EVM with its own dedicated chain-ID. Signed transactions are inputted, and a chain of blocks is produced as output. Similar to other built-in standard contracts (e.g., RLP encoding), this EVM environment can be imported into any Flow transaction or script.
franklywatson marked this conversation as resolved.
Show resolved Hide resolved

```
import EVM from <ServiceAddress>
```

Within the Flow transaction, if EVM interaction is successful

- it makes changes to the on-chain data
- forms a new block if successful,
- emits several Flow event types (see [here](https://github.com/onflow/flow-go/blob/master/fvm/evm/types/events.go)) that can be consumed to track the chain progress.
And if unsuccessful, it reverts the transaction.

As EVM interactions are encapsulated within Flow transactions, they leverage the security measures provided by Flow. These transactions undergo the same process of collection, execution, and verification as other Flow transactions, without any EVM intervention. Consequently, there is no requirement for intricate block formation logic (such as handling forks and reorganizations), mempools, or additional safeguards against malicious MEV (Miner Extractable Value) behaviours.
franklywatson marked this conversation as resolved.
Show resolved Hide resolved

In the EVM environment, resource consumption is metered as "gas usage". When interacting with the EVM environment, the total gas usage is translated back into Flow computation usage and is be paid as part of FLOW transaction fees (weigh-adjusted conversion).

#### EVM Addresses

In the EVM world, there is no concept of accounts or a minimum balance requirement. Any sequence of bytes with a length of 20 is considered a valid address.

Every EVM address has a balance of native tokens (e.g. ETH on Ethereum), a nonce (for deduplication) and a root hash of the state (if smart contract).
In this design, we use the FLOW token for this native token. The balance of an EVM address is stored as a smaller denomination of FLOW called `Atto-FLOW`, it works similarly to the way Wei is used to store ETH values on Ethereum.

```
access(all) contract EVM {

/// EVMAddress is an EVM-compatible address
access(all) struct EVMAddress {
Copy link
Contributor

@sisyphusSmiling sisyphusSmiling Dec 11, 2023

Choose a reason for hiding this comment

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

It would be helpful to have an easy comparator like a toString() method. Given the common pattern mapping addresses to values in EVM, I think it would be surprising to find that EVMAddress can't be used as a key in a mapping in Cadence.

Copy link
Member

Choose a reason for hiding this comment

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

Good idea to add a serialization!

What format would the function return? Hex-encoded? With 0x prefix or without?
IMHO the function should make that clear in the name, so there is no confusion. toString is too general

Copy link
Contributor

@sisyphusSmiling sisyphusSmiling Dec 12, 2023

Choose a reason for hiding this comment

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

What format would the function return? Hex-encoded? With 0x prefix or without?

Whichever format is returned should support deserialization into an EVMAddress as well. Since hex decoding doesn't support a prefix, I think we should exclude 0x. Though I wonder if foregoing the prefix will cause problems when passing values into EVM calls 🤔


/// Bytes of the address
access(all) let bytes: [UInt8; 20]

/// Constructs a new EVM address from the given byte representation
init(bytes: [UInt8; 20])
Copy link
Contributor

@sisyphusSmiling sisyphusSmiling Jan 4, 2024

Choose a reason for hiding this comment

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

I'm finding it frustrating to convert between [UInt8] and [UInt8; 20] when constructing EVMAddress and I think others will as well. For example, given some hex encoded address, I would want to construct an EVMAddress in a script to call a solidity contract. I would expect I could simply:

let evmAddress = EVMAddress(bytes: hexAddress.decodeHex())

Instead, I have to do:

let evmAddressBytes: [UInt8] = evmContractAddressHex.decodeHex()
let evmAddress = EVM.EVMAddress(
        bytes: [
            evmAddressBytes[0], evmAddressBytes[1], evmAddressBytes[2], evmAddressBytes[3], evmAddressBytes[4],
            evmAddressBytes[5], evmAddressBytes[6], evmAddressBytes[7], evmAddressBytes[8], evmAddressBytes[9],
            evmAddressBytes[10], evmAddressBytes[11], evmAddressBytes[12], evmAddressBytes[13], evmAddressBytes[14],
            evmAddressBytes[15], evmAddressBytes[16], evmAddressBytes[17], evmAddressBytes[18], evmAddressBytes[19]
        ]
    )

I think it would be preferable to abstract away the complexities of dealing with the restricted byte array in the struct's initialization.

Copy link
Member

Choose a reason for hiding this comment

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

It would be great to keep the type-safety, though I see and agree with the UX problems.

If the input type would be a variable-sized array, what would be the behaviour for input that is too short or too long? Developers might make assumptions, which could potentially be different from actual behaviour, and thus lead to costly mistakes.

For converting between constant-size and variable-size arrays, there is actually a Cadence feature request issue open: onflow/cadence#2530

Copy link
Contributor

Choose a reason for hiding this comment

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

I think a pre-condition like bytes.length == 20 in EVMAddress.init is behaviorally similar to the current implementation. Is that workable?

Copy link
Member

Choose a reason for hiding this comment

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

A pre-condition is a dynamic (run-time) check, and not a static one (type-checking time). It would be nice not to have to express the requirement of the length statically so errors can be prevented as early as possible


/// Returns the balance of this address
access(all) fun balance(): Balance

/// Deposits the given vault into the EVM account with the given address
access(all) fun deposit(from: @FlowToken.Vault)
ramtinms marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

A FLOW is equivalent of 10^18 atto-FLOW. Because EVM environments uses different way of storage of value for the native token than FLOW protocol (FLOW protocol uses a fixed point representation), to remove any room for mistakes, EVM environment has structure called `Balance` that could be used.

Note that no new FLOW token is minted and every balance on EVM addresses has to be deposited by bridging FLOW tokens into addresses using `deposit` method.

```
access(all)contract EVM {

access(all) struct Balance {
/// The balance in FLOW
access(all) let flow: UFix64

/// Constructs a new balance, given the balance in FLOW
init(flow: UFix64)

/// Returns the balance in terms of atto-FLOW.
/// Atto-FLOW is the smallest denomination of FLOW inside EVM
access(all) fun toAttoFlow(): UInt64
}
}
```

Every account on Flow EVM could be queried by constructing an EVM structure. Here is an example:

```
// Example of balance query
import EVM from <ServiceAddress>

access(all)
fun main(bytes: [UInt8; 20]) {
let addr = EVM.EVMAddress(bytes: bytes)
let bal = addr.balance()
}
```

#### EVM-style transaction wrapping

One of the design goals of this work is to ensure that existing EVM ecosystem tooling and products which builders use can integrate effortlessly. To achieve this, the EVM smart contract accepts RLP-encoded transactions for execution. Any transaction can be wrapped and submitted by any user through Flow transactions. As mentioned earlier, the resource usage during EVM interaction is translated into Flow transaction fees, which must be paid by the account that wrapped the original transaction.

To facilitate the wrapping operation and refunding, the run interface also allows a `coinbase` address to be passed. The use of the `coinbase` address in this context indicates the EVM address which will receive the gas usage * gas price (set in transaction). Essentially, the transaction wrapper behaves similarly to a miner, receives the gas usage fees on an EVM address and pays for the transaction fees.

Any failure during the execution would revert the whole Flow transaction.

```
// Example of tx wrapping
import EVM from <ServiceAddress>

access(all)
fun main(rlpEncodedTransaction: [UInt8], coinbaseBytes: [UInt8; 20]) {
let coinbase = EVM.EVMAddress(bytes: coinbaseBytes)
EVM.run(tx: rlpEncodedTransaction, coinbase: coinbase)
Copy link
Collaborator

Choose a reason for hiding this comment

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

not familiar with evm world, but I feel like EVM.run should return something to Cadence land to be useful somehow.

Copy link
Member

Choose a reason for hiding this comment

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

Good point!

I noticed that the FLIP was actually lacking the definition of EVM.run, so I added it in 6dbd64d.

@ramtinms I noticed that the current implementation does not yet return the documented boolean return value. Also, I think at some point there was a discussion about if the function should:

  • Not return a result value indicating success, but rather cause the outer Flow/Cadence transaction to abort (not proposing it, just documenting this was considered, and noting it should be part of the proposal;)
  • Return a result value, but not just a boolean value, but e.g. a struct with more information (reason / error message? what else could be returned?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunatly it doesn't return anything, but the calls on the COA will return a value.

Copy link
Member

Choose a reason for hiding this comment

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

Right, it makes sense that there is no value returned from executing an EVM transaction (just like executing a transaction on Flow does not return a value).

However, it might still be useful to return a value from the function that runs the transaction which indicates the "result" of the transaction, e.g. if it succeeded or failed, e.g. as a boolean, or a struct which contains more information about the transaction execution.

@ramtinms Could you maybe point me to where in the code (flow-go and geth) where/how an EVM transaction is run and what the result of it is?

Copy link
Member

Choose a reason for hiding this comment

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

The implementation of run currently aborts on any error: https://github.com/onflow/flow-go/blob/645923211a85641ade7d335c773d627390de4901/fvm/evm/handler/handler.go#L77.

Is that the intended behaviour? Then we should update the proposal and remove the current bool return value.

If it isn't: It looks like BlockView.RunTransaction returns a transaction result value (https://github.com/onflow/flow-go/blob/645923211a85641ade7d335c773d627390de4901/fvm/evm/emulator/emulator.go#L110-L112), types.Result, and we could expose (parts of) it to Cadence?

}
```

For example, a user might use Metamask to sign a transaction for Flow EVM and broadcast it to services that check the gas fee on the transaction and wrap the transaction to be executed.

Note that account nonce would protect against double execution of a transaction, similar to how other non-virtual blockchains prevent the minor from including a transaction multiple times.

#### Bridged accounts
franklywatson marked this conversation as resolved.
Show resolved Hide resolved

Another major goal for this work is seamless composability across environments. For this goal, we have introduced a new type of address a bridged account, to the EVM environment (besides EOA and Smart Contract accounts). This new address type is similar to EOA’s except instead of being tied to a public/private key it would be controlled by Cadence resources. Unlike EOAs which are created using the key presented by the wallet there is no corresponding EVM key present in a BridgedAccount. Any bridged account is interacted with through BridgedAccount resource and any Flow account or Cadence smart contract that holds this resource could interact with the EVM environment on behalf of the address that is stored in the resource.

```
access(all)contract EVM {

access(all) resource BridgedAccount {
Copy link
Contributor

@sisyphusSmiling sisyphusSmiling Dec 11, 2023

Choose a reason for hiding this comment

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

Apologies if this is out of scope for this FLIP - given that a BridgedAccount is a resource, what happens in the EVM environment if the resource is destroyed in the Flow environment? Can it be reconstructed? Is there currently a similar case where an address exists and suddenly no longer does in EVM and would this present any unstated consequences?

Copy link
Member

Choose a reason for hiding this comment

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

Destroying the BridgedAccount resource is basically equivalent to "throwing away the private key" for an account. Thought technically it could be reconstructed, how should that work – who/what proves ownership of the account?

where an address exists and suddenly no longer does in EVM
Can you elaborate? Address on which side?

Copy link
Contributor

@sisyphusSmiling sisyphusSmiling Dec 11, 2023

Choose a reason for hiding this comment

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

Thought technically reconstructed, how should that work – who/what proves ownership of the account?

While I take it as implied, I don't believe there are any stated guarantees that a BridgedAccount address is unique. From the creation method below, it looks address uniqueness would be handled by InternalEVM, is that right?

As far as reconstruction, I think the resource itself is sufficient to prove ownership and reconstructions isn't a desirable feature. It make sense to me that destruction would be akin to losing the keys.

Can you elaborate? Address on which side?

With the BridgedAccount destroyed, wouldn't the address no longer exist on either side? I think I overlayed an interface I saw earlier (not listed in this FLIP) which included a method EVMAddress.exists(): Bool which I had assume to return false once a BridgedAccount is destroyed.

Intuitively, I'd expect the address to persist in the EVM environment post-destruction and some artifact of the address in the Cadence environment - maybe in InternalEVM in whatever mechanism prevents duplication?

Copy link
Member

Choose a reason for hiding this comment

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

I don't believe there are any stated guarantees that a BridgedAccount address is unique.

Good point, that should be stated explicitly.

it looks address uniqueness would be handled by InternalEVM, is that right?

"InternalEVM" is an implementation detail and shouldn't be part of the proposal.

I now think that I might have misread your question – Where you asking if there is a way to regain access to an EVM account for which the bridged account was destroyed in terms of suggesting that should be possible (how I read the question at first), or are you worried and are implying that shouldn't be possible?

With the BridgedAccount destroyed, wouldn't the address no longer exist on either side? I think I overlayed an interface I saw earlier (not listed in this FLIP) which included a method EVMAddress.exists(): Bool which I had assume to return false once a BridgedAccount is destroyed.

Great question! @ramtinms how do you think this will/should work? This should be documented in the proposal.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was asking out of concern, thinking it shouldn't be possible out of concern for implications in the EVM environment.


access(self)
let addressBytes: [UInt8; 20]

/// constructs a new bridged account for the address
init(addressBytes: [UInt8; 20])

/// The EVM address of the bridged account
access(all)
fun address(): EVMAddress

turbolent marked this conversation as resolved.
Show resolved Hide resolved
/// Withdraws the balance from the bridged account's balance
access(all)
franklywatson marked this conversation as resolved.
Show resolved Hide resolved
fun withdraw(balance: Balance): @FlowToken.Vault

/// Deploys a contract to the EVM environment.
/// Returns the address of the newly deployed contract
turbolent marked this conversation as resolved.
Show resolved Hide resolved
access(all)
fun deploy(code: [UInt8], gasLimit: UInt64, value: Balance): EVMAddress

/// Calls a function with the given data.
/// The execution is limited by the given amount of gas
turbolent marked this conversation as resolved.
Show resolved Hide resolved
access(all)
fun call(to: EVMAddress, data: [UInt8], gasLimit: UInt64, value: Balance): [UInt8]
franklywatson marked this conversation as resolved.
Show resolved Hide resolved
turbolent marked this conversation as resolved.
Show resolved Hide resolved
}

/// Creates a new bridged account
access(all)
fun createBridgedAccount(): @BridgedAccount
}
```

Bridged account addresses are allocated by the FVM and stored inside the resource. Calls through bridged accounts form a new type of transaction for the EVM that doesn’t require signatures and doesn’t need nonce checking. Bridged accounts could deploy smart contracts or make calls to the ones that are already deployed on Flow EVM.
franklywatson marked this conversation as resolved.
Show resolved Hide resolved

Bridged accounts also facilitate the withdrawal of Flow tokens back from the EVM balance environment into the Cadence environment through `withdraw`.

**What about other fungible and non-tokens?**

The term "bridged accounts" is used because their design facilitates the building of bridges. A Cadence smart contract can control a bridged account, enabling transactional operations between two environments. For instance, this smart contract can receive a Fungible on the Cadence side and send the equivalent on the EVM side to a specified address, all in a single Flow transaction. More details on this will be provided in subsequent updates.

#### Safety and Reproducibility of the EVM state

EVM state is not accessible by Cadence outside of the defined interfaces above, and Cadence state is also protected against raw access from the EVM.
turbolent marked this conversation as resolved.
Show resolved Hide resolved

At the start, the EVM state is empty (empty root hash), and all the state is stored under a Flow account that is not controlled by anyone (network-owned). Any interaction with this environment emits a transactionExecuted event (direct calls for bridged accounts use `255` as transaction type).

So anyone following these events could reconstruct the whole EVM state by re-executing these transactions.

### Dependencies

The project introduces no new dependencies from the Ethereum codebase since those required are already included in flow-go

### Tutorials and Examples

A proof of concept implementation of this proposal is available [here](https://github.com/onflow/flow-emulator/releases/tag/v0.57.4-evm-poc).

What parts of the design still need to be defined?