|
| 1 | +# Standalone EVM |
| 2 | + |
| 3 | +## EVM as example |
| 4 | +In the early stages of the MultiversX VM development, there were already components built specifically for EVM compatibility. We are revisiting and reusing parts of that code. In **VM1.2**, for instance, there was a direct correspondence between EVM opcodes and the **BlockchainHook** interface, as well as a mechanism that wrapped MvX-style transaction data (**txData**) into EVM-specific `vmInput`. |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +## 1. VMExecutionHandlerInterface |
| 9 | +The MultiversX protocol defines a **VMExecutionHandlerInterface** with the following functions: |
| 10 | + |
| 11 | +```go |
| 12 | +// RunSmartContractCreate computes how a smart contract creation should be performed |
| 13 | +RunSmartContractCreate(input *ContractCreateInput) (*VMOutput, error) |
| 14 | + |
| 15 | +// RunSmartContractCall computes the result of a smart contract call and how the system must change after the execution |
| 16 | +RunSmartContractCall(input *ContractCallInput) (*VMOutput, error) |
| 17 | +``` |
| 18 | +The **SCProcessor** from `mx-chain-go` prepares the input information for these functions. We aim to avoid modifying the **SCProcessor** itself; instead, all necessary abstractions will be implemented at the EVM level. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 2. Input Preparation: EVMInputCreator |
| 23 | + |
| 24 | +When a contract creation request is made (via *ContractCreateInput), an EVMInputCreator component will: |
| 25 | +- Convert the `ContractCreateInput` into an EVMInput. |
| 26 | +- Invoke the actual EVM smart contract logic. |
| 27 | + |
| 28 | +The EVM itself is taken from the official Go implementation ([evm.go](https://github.com/ethereum/go-ethereum/blob/master/core/vm/evm.go) in go-ethereum). |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## 3. Abstraction Layer: MultiversX & EVM Interfaces |
| 33 | + |
| 34 | +To allow the EVM to function within MultiversX, we introduce a layer that bridges EVM interfaces with MultiversX components. The core interface it uses is the `BlockchainHookInterface`, which grants access to critical blockchain data, state, and transaction information. |
| 35 | +### 3.1 Reading & Writing to Storage |
| 36 | + |
| 37 | +- **Reading Storage**: When an EVM opcode attempts to read data from the storage (e.g., `readStorageFromTrie(key)`), it should invoke `blockchainHook.ReadFromStorage(scAddress, key)`. |
| 38 | + Internally, this call goes through the `storageContext` component, which manages reads from local cache if a key has already been accessed or modified during the current transaction. |
| 39 | + |
| 40 | +- **Writing to Storage**: When writing to storage, the EVM opcode should call `SetStorageToAddress(address, key)` in the `storageContext`. |
| 41 | + |
| 42 | +### 3.2 Finalizing State Changes |
| 43 | + |
| 44 | +After EVM execution finishes, we need to commit the resulting state changes to the blockchain. The EVM will use the `outputContext` component, which (together with the `storageContext`) tracks modified accounts and storages. It also creates the final `vmOutput`, which the `scProcessor` in `mx-chain-go` will then validate and apply to the blockchain (the trie) if everything is correct. |
| 45 | + |
| 46 | +--- |
| 47 | +## 4. Gas Metering |
| 48 | + |
| 49 | +EVM gas metering is handled internally within the EVM code. The VMExecutionHandler can receive a new gas schedule via: |
| 50 | + |
| 51 | +```go |
| 52 | +GasScheduleChange(newGasSchedule map[string]map[string]uint64) |
| 53 | +``` |
| 54 | + |
| 55 | +This function provides the cost of each opcode as a map. The EVM needs the appropriate wrapper functions to load these costs into its **OPCODES** structure. |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +## 5. Implementation Steps: Integrating EVM |
| 60 | + |
| 61 | +- Start from the SpaceVM code. |
| 62 | +- Replace the current executor (WASMER) with the EVM executor. |
| 63 | +- During EVM opcode interpretation, invoke the storageContext and meteringContext functions to manage state changes and track gas consumption. |
| 64 | + |
| 65 | +Once these steps are complete, the underlying EVM logic should effectively run on MultiversX. |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## 6. Address Conversion: 20 Bytes vs. 32 Bytes |
| 70 | + |
| 71 | +EVM addresses are 20-bytes long, whereas MultiversX uses 32-byte addresses. To avoid changing the broader MultiversX system, the EVM will use internal transformers: |
| 72 | + |
| 73 | +- **Internal EVM Calls**: Within the EVM, contracts use the last 20 bytes of the corresponding 32-byte MvX address. |
| 74 | +- At runtime, the full 32-byte address is still known, and when a storage `read` or `write` occurs, the EVM prefixes the last 20 bytes with 10 bytes of zeros plus a 2-byte `VMType` (the standard MvX smart contract addressing scheme). |
| 75 | + |
| 76 | +### 6.1 Calling EVM Contracts from EVM |
| 77 | + |
| 78 | +When an EVM-based smart contract calls another EVM contract, it uses the 20-byte address. Internally, the system prefixes these 20 bytes with the deterministic overhead (10 bytes of zeros and 2 bytes for `VMType`) to fetch the appropriate contract code from the accounts trie before running it. |
| 79 | + |
| 80 | +### 6.2 Token Storage in EVM |
| 81 | + |
| 82 | +Token balances (like ERC20) live in the contract’s own storage. The contract will use the last 20 bytes of a user’s MvX address when recording ownership or balances. If an opcode like `GetCaller` is executed, it returns only the last 20 bytes from the `ContractCallInput.Sender`. |
| 83 | + |
| 84 | +### 6.3 Calling WasmVM from EVM |
| 85 | + |
| 86 | +MultiversX WasmVM expects 32-byte addresses. If an EVM contract tries to invoke a WasmVM contract using only 20 bytes, the call will fail due to incorrect argument size. Consequently, when the EVM calls a WasmVM contract, it must supply a full 32-byte address. |
| 87 | + |
| 88 | +:::note |
| 89 | +In most cases, the EVM contracts will call only other EVM contracts. However, bridging to WasmVM is still feasible, for example, when claiming ESDT tokens through an ERC wrapper contract. |
| 90 | +::: |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## 7. WASM VM and the `ExecuteOnDestOnOtherVM` Function |
| 95 | + |
| 96 | +The **WASM VM** supports a public function `ExecuteOnDestOnOtherVM` via the **BlockchainHook** interface. If a new VM is fully integrated, it can be added to the `vmContainer` component with a new **baseAddress**. Below is an example table illustrating potential base addresses for different VMs: |
| 97 | + |
| 98 | +| VM Name | Example Base Address | Notes | |
| 99 | +|-------------|----------------------|----------------------------------------------------------------------------------| |
| 100 | +| **WASM VM** | 05 | Standard base address for the WASM VM | |
| 101 | +| **System VM** | 255 | Standard base address (example) for the System VM | |
| 102 | +| **EVM** | To Be Decided | Will be assigned upon integration to ensure address derivation works properly | |
| 103 | + |
| 104 | +From the `SCAddress`, the protocol looks at bytes **10** and **11** to determine which VM should be called. Once EVM integration is complete, it will receive its own base address and will adjust how the **CreateContract** opcode calculates deployed contract addresses. |
| 105 | + |
| 106 | +### 7.1 Synchronous Execution |
| 107 | + |
| 108 | +When the EVM executes a `DelegateCall` opcode, it will invoke an internal function of the new EVM implementation that checks whether execution should occur in the EVM itself or a different VM. If it needs to run on another VM, it calls `blockchainHook.ExecuteOnDestOnOtherVM`. |
| 109 | + |
| 110 | +- **Returning `VMOutput`**: This function returns a `VMOutput`, which can be merged into the current `outputContext` and `storageContext` via the `PushContext`-type public functions. |
| 111 | + |
| 112 | +In the **WASM VM**, if a smart contract calls `ExecuteOnDest`, the VM decides where the execution should take place. For asynchronous calls, the same logic applies: |
| 113 | + |
| 114 | +- **Intra-Shard**: The system calls `ExecuteOnDestOnOtherVM`. |
| 115 | +- **Cross-Shard**: On the destination shard, the **scProcessor** determines which VM to invoke and continues accordingly. |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## 10. ESDT ↔ ERC20 & ESDTNFT ↔ ERC721 |
| 120 | + |
| 121 | +Bridging MultiversX ESDT standards with common Ethereum-based token standards (ERC20, ERC721, etc.). This introduces several key differences in token handling, especially around **token transfers** and **approval mechanisms**. |
| 122 | + |
| 123 | +### 10.1 ESDT Transfer Model |
| 124 | +On MultiversX, transfers typically use a **`transferAndExecute`** paradigm: |
| 125 | +- The sender (token owner) explicitly initiates a transfer of tokens and, in the same operation, calls a smart contract endpoint to process further actions (e.g., swapping, staking, etc.). |
| 126 | + |
| 127 | +### 10.2 ERC20 Transfer Model |
| 128 | +In the Ethereum ecosystem, the common workflow is: |
| 129 | +1. **Approval**: A user grants a smart contract (SC) permission to spend tokens on their behalf by calling `approve(scAddress, amount)`. |
| 130 | +2. **Transfer**: The SC (now approved) calls `transferFrom(user, destination, amount)` to pull tokens from the user’s balance. |
| 131 | + |
| 132 | +This design allows third-party contracts to move funds from a user’s wallet without a new, explicit approval each time. However, it also opens the door to potential exploits: a malicious dApp can trick users into granting excessive approvals, which might be exploited later to drain funds. |
| 133 | + |
| 134 | +### 10.3 The Wrapper/SafeESDT Contract |
| 135 | + |
| 136 | +Because MultiversX prohibits direct “pull” transfers of ESDTs (a fundamental security decision), bridging to ERC-like workflows requires an **intermediary contract**—often called a **wrapper** or **safeESDT** contract: |
| 137 | + |
| 138 | +1. **Deposit**: A user deposits their ESDT tokens into the wrapper contract. |
| 139 | +2. **Allow**: The user can specify which addresses (e.g., other SCs) are allowed to withdraw a certain amount of these deposited tokens. |
| 140 | +3. **Transfer**: The contract implements an ERC20-like `transferFrom()` functionality. When an external EVM-based SC tries to “pull” tokens, it actually interacts with this safeESDT contract, which checks permissions and only then completes the transfer if authorized. |
| 141 | +4. **Withdrawal**: The user can reclaim any unspent tokens from the wrapper contract when they wish. |
| 142 | + |
| 143 | +In the EVM environment, an operation like `safeESDTContract.transferFrom(user, scAddress, amount)` would mimic the ERC20 approach. Under the hood, the **blockchainHook** would manage a synchronous call to the other VM. |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +### 10.4 Extending to Other ERC Standards |
| 148 | + |
| 149 | +A similar wrapper approach can be adopted for other token types: |
| 150 | + |
| 151 | +- **ERC721 (NFTs)**: An **ESDTNFT** wrapper can track ownership and minted tokens, providing `approve()` and `transferFrom()` methods that mirror standard ERC721 functionality. |
| 152 | +- **ERC1155**: This multi-token standard can likewise be “wrapped,” allowing ESDT-based multi-tokens to be interfaced with EVM-based dApps expecting ERC1155 contracts. |
| 153 | + |
| 154 | +By handling all “pull” transfers inside dedicated wrapper contracts, MultiversX preserves its **secure-by-design** “push” transfer model while still enabling compatibility with dApps that rely on ERC-style approvals. |
| 155 | + |
| 156 | +### Claiming ESDT Tokens from an ERC20 Balance |
| 157 | + |
| 158 | +This diagram illustrates how a user claims an ESDT token originally held in an ERC20 contract on the EVM side. The process involves burning ERC20 tokens, calling a WASM VM wrapper contract, and finally minting ESDT tokens to the user. |
| 159 | + |
| 160 | +```mermaid |
| 161 | +sequenceDiagram |
| 162 | + participant U as User |
| 163 | + participant E as ERC20 Contract (EVM) |
| 164 | + participant W as WASM VM Wrapper |
| 165 | +
|
| 166 | + U->>E: 1) Call ERC20 contract to burn tokens |
| 167 | + E->>W: 2) callContract(ERCWrapper) on WASM VM<br>(includes burn details) |
| 168 | + Note over W: Registers the token under a 20-byte address<br>(EVM only knows 20 bytes) |
| 169 | + U->>W: 3) User claims tokens from the WASM VM wrapper |
| 170 | + W->>W: 3a) Checks last 20 bytes == callInput.CallerAddress[12:32] |
| 171 | + W->>U: 4) Mints and sends ESDT tokens to OriginalCaller |
| 172 | +``` |
0 commit comments