FIP-0054 Source

TitleFilecoin EVM runtime (FEVM)
AuthorRaúl Kripalani, Steven Allen
Discussions-Tohttps://github.com/filecoin-project/FIPs/discussions/592
StatusAccepted
TypeTechnical Core
CategoryCore
Created2022-12-02

Spec Sections

FIP-0054: Filecoin EVM compatibility (FEVM)

Table of Contents generated with DocToc

Simple Summary

We introduce a new built-in actor: the Filecoin EVM (FEVM) runtime actor, capable of running EVM smart contracts on top of the Filecoin Virtual Machine. We also introduce various changes to the Filecoin Virtual Machine and to client implementations, necessary to support the operation of this new actor.

Abstract

The Filecoin EVM (FEVM) runtime built-in actor runs EVM smart contracts compatible with the Ethereum Paris fork, plus EIP-3855 (PUSH0).

It achieves this by embedding a special-purpose EVM interpreter, implementing the integration logic with the Filecoin environment, translating environment-dependent opcodes into their corresponding Filecoin primitives, and mapping all state I/O to the underlying IPLD model.

The EVM interpreter strives for maximal as-is portability of existing EVM bytecode. For this reason, the EVM interpreter supports all required opcodes and Ethereum precompiles to strive for maximal portability. Functional and technical departures from Ethereum’s standard expectations are documented herein.

The FEVM runtime actor motivates some changes in the FVM. Syscalls are modified, syscalls are added, new environmental data is required, and new exit codes are created. The concept of a TipsetCID is also introduced formally, and is of required implementation by clients.

This FIP is dependent on FIP-0048 (f4 address class), FIP-0049 (Actor events), and FIP-0055 (Supporting Ethereum Accounts, Addresses, and Transactions).

Change Motivation

A basic requirement to achieve EVM compatibility is to be able to run EVM smart contracts. Given the inability to deploy user-defined Wasm actors (arriving at a later stage of the FVM roadmap), we introduce this capability by adding a new built-in actor to the network. This actor is accompanied by FVM and client changes necessary for functional completeness.

Specification: Filecoin EVM (FEVM) Runtime Actor

The FEVM runtime actor is the Filecoin built-in actor that hosts and executes EVM bytecode. It contains:

  1. An embedded EVM interpreter. It processes EVM bytecode handling every instruction according to EVM expectations, with functional departures described herein.
  2. The integration logic connecting the EVM interpreter with the Filecoin environment, chain, and virtual machine (FVM).
  3. A collection of Ethereum and Filecoin-specific precompiles, as specified below.

Installation and wiring

On the migration for the network upgrade where this FIP goes live:

  1. The Wasm bytecode for the EVM runtime actor is loaded onto the node’s blockstore.
  2. Its CodeCID is linked to the System actor’s state under the key "evm".
  3. An Ethereum Account actor (as per FIP-0055) is created in the state tree with balance zero, nonce zero, and delegated address f410faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaonc6iji, corresponding to the Ethereum zero address.

Instantiatable actor

The Filecoin EVM runtime is an actor that can be instantiated by users (and therefore, not a singleton actor). A new instance is to be created for each EVM smart contract deployed on the network. Instantiation can only originate in the Ethereum Address Manager (EAM), as defined in FIP-0055, who assigns an f410 subaddress prior to creating the actor via the Init actor, as per standard Filecoin mechanics.

At the time of writing, this makes the EVM runtime actor comparable to the Miner, Payment Channel, and Multisig built-in actors, and unlike the Storage Power, Storage Market, Cron, and other singleton built-in actors.

State

The state of the EVM runtime actor is as follows:

/// DAG-CBOR tuple-encoded.
pub struct State {
    /// The EVM contract bytecode resulting from calling the
    /// initialization code (init code) by the constructor.
    pub bytecode: Cid,

    /// The EVM contract bytecode hash keccak256(bytecode)
    pub bytecode_hash: [u8; 32],

    /// The EVM contract state dictionary.
    /// All EVM contract state is a map of U256 -> U256 values.
    ///
    /// Root of KAMT<U256, U256>
    pub contract_state: Cid,

    /// The EVM nonce used to track how many times CREATE or
    /// CREATE2 have been called.
    pub nonce: u64,

    /// Possibly a tombstone if this actor has been self-destructed.
    /// In the EVM, self-destructed contracts are "alive" until the current top-level transaction
    /// ends. We track this by recording the origin and nonce.
    ///
    /// Specifically:
    ///
    /// 1. On SELFDESTRUCT, they mark themselves as "deleted" (by setting a tombstone with the
    ///    current origin/nonce), send away all funds, and return immediately.
    /// 2. For the rest of the current transaction (as long as the tombstone's origin/nonce matches
    ///    the currently executing top-level transaction), the contract continues to behave
    ///    normally.
    /// 3. After the current transaction ends, the contract behaves as if it were an "empty"
    ///    contract, kind of like a placeholder. At this point, the contract can be "resurrected"
    ///    (recreated) by via CREATE/CREATE2.
    ///
    /// See https://github.com/filecoin-project/ref-fvm/issues/1174 for some context.
    pub tombstone: Option<Tombstone>
}

/// A tombstone indicating that the contract has been self-destructed.
/// DAG-CBOR tuple-encoded.
pub struct Tombstone {
    /// The message origin when this actor was self-destructed.
    pub origin: ActorID,
    /// The message nonce when this actor was self-destructed.
    pub nonce: u64,
}

Contract storage (KAMT)

EVM storage (u256 => u256 map) is backed by a specialized data structure based on the Filecoin HAMT: the fixed size Keyed AMT (KAMT). This data structure is more mechanically sympathetic to typical EVM storage read and write patterns (refer to Design Rationale for more detail). SLOAD and SSTORE EVM opcodes map to reads and writes onto this KAMT, respectively, and ultimately to ipld syscalls specified in [FIP-0030].

This is how the KAMT compares to its Filecoin HAMT:

  1. Key length. The HAMT uses 256-bit hashed keys. With the KAMT it is possible to specify an arbitrary key length.
  2. Key content. The HAMT assumes arbitrary keys, and hashes them on read and writes. The KAMT obviates the need for key hashing and assumes the caller has hashed keys to a fixed-width binary format.
  3. Overlapping prefixes and extensions. The HAMT assumes hashing function that results in unpredictable keys which are uniformly distributed across the keyspace. The KAMT optimizes for long overlaps in keys. Whenever a node in the data structure is full, it looks for the longest common prefix of the keys and uses extensions to skip multiple levels in the tree that would otherwise only contain a single pointer pointing at the next level.

Contrary to traditional EVM storage, the KAMT is an enumerable data structure. However, no operations are provided for smart contracts to enumerate keys, at least not at this stage. This useful property merely facilitates external observability and debuggability.

Actor interface (methods)

The Filecoin EVM runtime actor offers these entrypoints, each of which handles messages sent with the specified Filecoin method numbers. Method numbers respect the FRC42 calling convention.

Constructor (method number 1)

Initializes a new EVM smart contract with some init bytecode supplied as a constructor parameter.

It is only be invocable by the Ethereum Address Manager (EAM), specified in FIP-0055, indirectly via the Init actor. This precondition is validated by checking that an f4 address with namespace 10 (corresponding to the EAM) was assigned to the current actor, prior to its Constructor method being called.

The Constructor runs the init bytecode with the EVM interpreter, and populates the actor’s state accordingly:

  • bytecode field: the execution bytecode is stored as a raw IPLD block and linked by CID from this field.
  • contract_state field: contains the root CID of the storage KAMT resulting from SSTORE operations during construction.
  • nonce field: set to 0, unless any calls to CREATE or CREATE2 happen during initialization.
  • bytecode_hash: the Keccak-256 hash of the bytecode.
  • tombstone: may be non-empty if the contract selfdestructed itself during construction.

Input parameters

// CBOR tuple encoded.
pub struct ConstructorParams {
    /// The actor's "creator" (specified by the EAM).
    pub creator: EthAddress,
    /// The EVM initcode that will construct the new EVM actor.
    pub initcode: RawBytes,
}

Return value

None.

Errors

  • USR_ASSERTION_FAILED (exit code 24) if the caller does not have an f4 delegated address under namespace 10 (that of the Ethereum Address Manager), or if it’s lacking a subaddress.
  • USR_ILLEGAL_ARGUMENT (exit code 16) if the EVM execution bytecode is longer than 24KiB (same limit as Ethereum), or if the bytecode starts with 0xef (EIP-3541).
  • USR_ILLEGAL_STATE (exit code 20) if we failed to hash, or write the bytecode to the blockstore.
  • EVM_CONTRACT_REVERTED (exit code 33) if the init bytecode reverted.

Resurrect (method number 2)

Reinstates a contract at a previously self-destructed actor. This is a special version of the Constructor that the EAM invokes directly (not through the Init actor) when an EVM smart contract already existed at the address.

Input parameters

Equal to Constructor.

Return value

Equal to Constructor.

Errors

Same as Constructor.

  • USR_ASSERTION_FAILED (exit code 24) if the caller does not have an f4 delegated address under namespace 10 (that of the Ethereum Address Manager), or if it’s lacking a subaddress.
  • USR_ILLEGAL_ARGUMENT (exit code 16) if the EVM execution bytecode is longer than 24KiB (same limit as Ethereum), or if the bytecode starts with 0xef (EIP-3541).
  • USR_ILLEGAL_STATE (exit code 20) if we failed to hash, or write the bytecode to the blockstore.
  • EVM_CONTRACT_REVERTED (exit code 33) if the init bytecode reverted.

GetBytecode (method number 3)

Returns the CID of the contract’s EVM bytecode block, adding it to the caller’s reachable set. This method is used internally to resolve EXTCODE* opcodes.

Input parameters

None.

Return value

Nillable CID of the contract’s bytecode as stored in state. Returns nil if the contract is dead.

Errors

  • USR_ILLEGAL_ARGUMENT (exit code 16) when we fail to load the state.

GetBytecodeHash (method number 4)

Returns the keccak-256 hash of the execution bytecode, which was computed at construction time and stored in state.

Input parameters

None.

Return value

Keccak-256 hash of the contract’s bytecode as stored in state.

Errors

  • USR_ILLEGAL_ARGUMENT (exit code 16) when we fail to load the state.

GetStorageAt (method number 5)

Returns the value stored at the specified EVM storage slot. This method exists purely for encapsulation purposes; concretely, to enable tools to inspect EVM storage without having to parse the state object, or understand the KAMT. Calling is restricted to f00 (system actor), and therefore cannot be invoked via messages or internal sends. It can be used by the Ethereum JSON-RPC eth_getStorageAt operation to resolve requests by constructing a local call and processing it with the FVM.

Input parameters

// CBOR tuple encoded.
pub struct GetStorageAtParams {
    pub storage_key: U256, // encoded as a CBOR byte string
}

Return value

Storage value (U256), encoded as a CBOR byte string. Returns a 0-filled 32-byte array, if the key doesn’t exist, or if the contract is dead.

Errors

  • USR_FORBIDDEN (exit code 18) when not called by address f00.
  • USR_ASSERTION_FAILED (exit code 24) on internal errors.
  • USR_UNSPECIFIED (exit code 23) when we fail to get the storage slot due to an internal error.

InvokeContractDelegate (method number 6)

Recursive invocation entrypoint backing calls made through the DELEGATECALL opcode. Only callable by self. See remarks on the DELEGATECALL opcode below for more information.

Input parameters

// CBOR tuple encoded.
pub struct DelegateCallParams {
    pub code: Cid,
    /// The contract invocation parameters
    /// Encoded as a CBOR byte string
    pub input: Vec<u8>,
    /// The original caller's Eth address.
    pub caller: EthAddress,
    /// The value passed in the original call.
    pub value: TokenAmount,
}

Return value

Same as InvokeContract.

Errors

  • Exit code USR_FORBIDDEN (18) when caller is not self.

InvokeContract (method number 38444508371)

Invokes an EVM smart contract by loading the execution bytecode from state, and dispatching input data to it.

The input data is expected to be framed as a CBOR byte string. This method unframes it before handing it over to the contract (subsequently retrievable through CALLDATA* opcodes). This method is universally callable.

Input parameters

Raw bytes, encoded as a CBOR byte string.

Return value

Raw bytes, encoded as a CBOR byte string.

Errors

  • USR_UNSPECIFIED (exit code 23) on internal errors instantiating the EVM runtime.
  • USR_NOT_FOUND (exit code 17) when failing to load the bytecode.
  • EVM_CONTRACT_REVERTED (exit code 33) when the contract reverts.
  • EVM_CONTRACT_INVALID_INSTRUCTION (exit code 34) the INVALID instruction was executed.
  • EVM_CONTRACT_UNDEFINED_INSTRUCTION (exit code 35) an undefined instruction was executed.
  • EVM_CONTRACT_STACK_UNDERFLOW (exit code 36) there was a stack underflow when executing an instruction.
  • EVM_CONTRACT_STACK_OVERFLOW (exit code 37) there was a stack overflow when executing an instruction.
  • EVM_CONTRACT_ILLEGAL_MEMORY_ACCESS (exit code 38) there was an illegal memory access.
  • EVM_CONTRACT_BAD_JUMPDEST (exit code 39) the contract jumped to an illegal offset.
  • Any other error that is raised during bytecode execution.

HandleFilecoinMethod (general handler for method numbers >= 1024)

Filecoin native messages carrying a method number above or equal to 1024 (a superset of the public range of the FRC42 calling convention) are processed by this entrypoint. This path enables processing transactions sent from non-Ethereum sending sites, e.g. built-in actors, Filecoin wallets, and future Wasm actors, with arbitrary Filecoin method numbers.

Calling this entrypoint performs the following logic:

  1. Map the Filecoin native message to Solidity input data satisfying the signature below.
  2. Hand off execution to the EVM bytecode.
  3. If execution succeeds, interpret the return data according to the signature below, and call the fvm::exit syscall with the extracted exit code and return data.

When called with no parameters, the input codec will be 0 and the input params will be empty. To return no result, return the codec 0 and empty return_data.

// Function selector: 0x868e10c4
// Note: return parameters have been named for specification purposes only.
// Naming return parameters is not valid Solidity.
function handle_filecoin_method(uint64 method, uint64 codec, bytes calldata params) public
   returns (exit_code uint32, codec uint64, return_data bytes memory) {}

For more information, see the Solidity Contract ABI Specification for more details about the call convention, parameter layouts, and packing.

Input parameters

Limited to nothing or a valid CBOR object (for now). In the future, other codecs will be supported.

Return value

Limited to nothing or a valid CBOR object (for now). In the future, other codecs will be supported.

Addressing

EVM smart contracts deployed on Filecoin calling opcodes that take addresses as parameters can use two types of addresses. Both these addresses conform to EVM addressing expectations (160-bit width):

  1. Masked Filecoin ID addresses.
  2. Actual Ethereum addresses.

Masked Filecoin ID addresses are addresses conforming to Ethereum’s type (160-bit) that encode Filecoin ID addresses. They are distinguished through a byte mask. The high byte is 0xff (discriminator), followed by thirteen 0 bytes of padding, then the uint64 ID of the actor.

|- disc -|    |----- padding ------|    |----- actor id -----|
0xff       || 0000000000000000000000 || <uint64 ID big endian>

Ethereum addresses are all addresses that do not satisfy the above mask. They are converted to f410 addresses as per the conversion specified in FIP-0055. This includes the Ethereum Zero Address and precompile addresses.

Opcode support

All opcodes from the [Ethereum Paris hard fork] are supported, plus PUSH0 from EIP-3855. This section enumerates all supported opcodes, noting functional departures from their Ethereum counterparts, as well as relevant remarks about how they operate. Opcodes are referred to by their mnemonic name.

Opcodes without remarks

These opcodes are handled locally within the EVM interpreter and have no departures from their original behaviour in Ethereum.

  • Arithmetic family: ADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND.
  • Boolean operators: LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, XOR, NOT.
  • Bitwise operations: BYTE, SHL, SHR, SAR.
  • Control flow: STOP, JUMPDEST, JUMP, JUMPI, PC, STOP, RETURN, INVALID.
  • Parameters: CALLDATALOAD, CALLDATASIZE, CALLDATACOPY, RETURNDATASIZE, RETURNDATACOPY.
  • Stack manipulation: POP, PUSH{0..32}, DUP{1..32}, SWAP{1..16}.

Opcodes with remarks

Memory: MLOAD, MSTORE, MSTORE8, MSIZE. EVM memory is modelled as an object inside the interpreter, ultimately backed by 32-bit Wasm memory. Usage of of these instructions incurs in Wasm memory expansion costs and is bounded by the memory limits specified in FIP-0057. Contrary to Ethereum, Filecoin does enforce a maximum memory limit. In Ethereum, memory is bounded implicitly by gas. A failure to allocate will cause the system to abort with the SYS_ILLEGAL_INSTRUCTION exit code.

Accessors: ADDRESS. Returns the Ethereum address of the executing contract.

Accessors: BALANCE. Returns the filecoin balance of the contract, in atto precision (same precision as Ethereum).

Accessors: ORIGIN. Returns the Ethereum address of the account where the chain message originated.

Accessors: CALLER. Returns the Ethereum address of the immediate caller of the contract.

Accessors: CALLVALUE. Returns the filecoin value sent in the message, in atto precision (same precision as Ethereum).

Accessors: GASPRICE. Returns the base fee plus the gas premium, in atto precision (same precision as Ethereum). This is functionally equivalent to Ethereum’s GASPRICE definition following EIP-1559, which is to return the effective_gas_price, defined as the following:

effective_gas_price = effective_gas_premium + BaseFee

In Filecoin, the effective_gas_premium is bounded by the Message.GasFeeCap and the BaseFee and clamped at 0:

effective_gas_premium = max(min(Message.GasPremium, Message.GasFeeCap - BaseFee), 0)

Accessors: BLOCKHASH. Returns the hash within the tipset CID of the requested epoch, with a maximum lookback of 256 epochs, truncated to preserve the left-most 32 bytes. If the requested epoch is a null round, it seeks to the last non-null tipset. This means that the lookup can range beyond 256 epochs if null rounds are involved.

Accessors: COINBASE. Nil implementation. Returns a fixed 0x0 value.

Accessors: TIMESTAMP. Returns the timestamp of the epoch being executed, as a Unix timestamp, as supplied by the client during Machine construction.

Accessors: NUMBER. Returns the epoch being executed.

Accessors: PREVRANDAO (former DIFFICULTY, see EIP-4399). Returns beacon randomness for the current epoch, by calling the rand::get_beacon_randomness syscall as defined in [FIP-0030], supplying:

  • domain separation tag 10
  • entropy equivalent to the UTF-8 bytes of string “prevrandao”

Accessors: GASLIMIT. Returns the fixed number 10e9 (10B gas).

Accessors: CHAINID. Returns the EIP-155 Chain ID of the current network.

Accessors: BASEFEE. Returns the base fee, in atto precision (same precision as Ethereum).

Accessors: SELFBALANCE. Returns the contract’s filecoin balance, in atto precision (same precision as Ethereum).

Accessors: GAS. Returns the Filecoin gas available.

Control flow: REVERT. Aborts by calling the fvm::exit syscall with user-space exit code 33 (EVM_CONTRACT_REVERTED), passing in the supplied value to be returned as return data, and reverting all changes.

Hashing: KECCAK256. Makes a syscall to crypto::hash with the supplied preimage and the Keccak-256 multihash.

Logging: LOG{0..4}. LOG{0..4} opcodes emit FIP-0049 compliant actor events conforming to the following template:

// Values are encoded as DAG-CBOR byte strings.
ActorEvent {
    entries: [
        (0x03, "t1", <first topic word>),   // when LOG1, LOG2, LOG3, LOG4
        (0x03, "t2", <second topic word>),  // when LOG2, LOG3, LOG4
        (0x03, "t3", <third topic word>),   // when LOG3, LOG4
        (0x03, "t4", <fourth topic word>),  // when LOG4
        (0x03, "d", <data word>),           // event data, omitted with LOG0
    ],
}

All fields are indexed by key and value because standard Ethereum queries against log data need to take the order of topics into account, as well as identify the data.

Storage: SLOAD, SSTORE. Maps storage operations to underlying reads and writes on the contract storage KAMT, which eventually result in ipld syscalls.

Lifecycle: CREATE. Calls the Create method of the Ethereum Address Manager (FIP-0055) to deploy the smart contract, applying the 1/64th gas reservation rule (EIP-150) to the entire call .

Lifecycle: CREATE2. Calls the Create2 method of the Ethereum Address Manager (FIP-0055) to deploy the smart contract, applying the 1/64th gas reservation rule (EIP-150) to the entire call.

Lifecycle: SELFDESTRUCT.

  1. Marks the current contract as deleted by setting the tombstone in state, recording the current message origin and nonce.
  2. Transfers the balance out to the designated beneficiary.
  3. Returns immediately.

The contract isn’t considered to be “dead” until it has a tombstone where the recorded origin and nonce differ from the origin and nonce of the currently executing message. This respects Ethereum semantics in that the contract continues to be “alive” until the end of the transaction.

  • Subsequent calls to the self-destructed contract within the same transaction will continue to run the existing bytecode.
  • Calls to this contract in subsequent transactions will return successfully with no code being executed.

Calls: CALL.

First, if the recipient is a precompile, FEVM invokes the precompile directly as follows:

  1. It invokes the precompile.
    1. The call_actor precompile applies the specified gas limit (if any) to the subsequent actor call.
    2. All other precompiles ignore the specified gas limit.
  2. On success, if the user has requested a non-zero value transfer, FEVM transfers the funds to the precompile’s Filecoin address (which will auto-create a Placeholder at that address, if necessary).

Otherwise, FEVM converts the CALL operation into a Filecoin “send” as follows:

  1. The method number is always EVM::InvokeContract (3844450837).
  2. The receiver’s address is converted into a Filecoin address.
  3. The input data is treated as IPLD_RAW if non-empty, or the empty block (block 0) if empty.
  4. The value is treated as a Filecoin token amount, as per Native currency.
  5. The gas limit is computed as follows:
    1. If the value zero and the gas limit is 2300, or the gas limit is zero and the value is non-zero, FEVM sets the gas limit to 10M. This ensures that bare “transfers” continue to work as 2300 isn’t enough gas to perform a call on Filecoin. 10M was picked to be enough to “always work” but not so much that it becomes a security concern.
    2. FEVM then applies the 1/64th rule from Ethereum, limiting the the sent gas to at most 63/64 of the remaining gas.
  6. The send flags are set to the default value (0).

Note that all gas values are Filecoin gas, not Ethereum gas. See Product considerations: Gas for more information.

Calls: CALLCODE. Not supported. Aborts with EVM_CONTRACT_UNDEFINED_INSTRUCTION (exit code 36) when used. This is because EIP-7 introduced DELEGATECALL as a hardened replacement for CALLCODE. Solidity no longer supports CALLCODE.

Calls: DELEGATECALL.

DELEGATECALL behaves differently depending on the recipient:

  • If the target is a precompile address, it is handled internally according to the precompile logic defined in CALL above.
  • If the target actor is an EVM runtime actor, it fetches the bytecode of the target and calls InvokeContractDelegate on itself to create a new call stack “frame” whilst still operating on our own state.
  • If the target is an account, placeholder, or an EthAccount actor (FIP-0055), it returns nothing and success (pushes 1 onto the stack).
  • If the target is any other type of actor, it returns nothing and failure (pushes 0 onto the stack).

Calls: STATICCALL. Performs the CALL in read-only mode, disallowing state mutations, event emission, and actor deletions. Specifically, it sets the “read-only” bit in the send flags (bit 0x1). See send::send for more information.

External code: EXTCODESIZE.

  • If the supplied address belongs to an EVM smart contract, it retrieves the size of the bytecode by calling the GetBytecode method, fetching the block via an ipld::block_read operation, and returning the length of the block.
  • If the supplied address is deemed an account, whether a native account, a placeholder, or an Eth account, it returns 0 (no code).
  • If the supplied address does not exist, it returns 0 (no code).
  • Otherwise, it returns the non-zero sentinel value 1, to indicate that there is code. However, it’s not EVM bytecode so we can’t return the real size.

NOTE: this operation is used by popular utilities to check if an address is a contract, e.g. OpenZeppelin’s toolbox.

External code: EXTCODECOPY.

  • If the supplied address belongs to an EVM smart contract, it copies its bytecode into the designated memory region by calling the GetBytecode method, fetching the block via an ipld::block_read operation, and copying it into the memory segment.
  • If the supplied address is deemed an account, whether a native account, a placeholder, or an Eth account, it copies nothing.
  • If the supplied address does not exist, it copies nothing.
  • Otherwise, it copies the sentinel value 0xfe.

External code: EXTCODEHASH.

  • If the supplied address belongs to an EVM smart contract, it copies its bytecode into the designated memory region by calling the GetBytecode method, fetching the block via an ipld::block_read operation, and copying it into the memory segment.
  • If the supplied address is deemed an account, whether a native account, a placeholder, or an Eth account, it copies nothing.
  • If the supplied address does not exist, it copies nothing.
  • Otherwise, it copies the sentinel value 0xfe.

Precompiles

There are two kinds of precompiles available to EVM smart contracts running within the FEVM runtime actor:

  • Ethereum precompiles
  • Filecoin precompiles

Ethereum precompiles

The FEVM runtime actor supports all Ethereum precompiles available in the Ethereum Paris fork. These precompiles sit at their original Ethereum addresses. There are no functional departures with respect to their original behaviors. However, gas accounting follows Filecoin mechanics instead of Ethereum’s. See the Product considerations: gas section for more information. Refer to Ethereum documentation for more information.

Ethereum Address Precompile
0x01 ecRecover
0x02 SHA2-256
0x03 RIPEMD-160
0x04 identity
0x05 modexp
0x06 ecAdd
0x07 ecMul
0x08 ecPairing
0x09 blake2f

Filecoin precompiles

The following Filecoin-specific precompiles are exposed to EVM smart contracts at the designated addresses:

Ethereum Address Precompile
0xfe00..01 resolve_address
0xfe00..02 lookup_delegated_address
0xfe00..03 call_actor
0xfe00..05 call_actor_id

resolve_address precompile

Reads a Filecoin address and resolves it into a BE-encoded u64 actor ID, or an empty array if nothing found. A failure to parse the address will result in a 1 being placed on the stack, which leads high-level languages like Solidity to REVERT.

Input data layout

<address> as bytes

Return data layout

<actor id> as u64 BE bytes (left padded to u256)

lookup_delegated_address precompile

Reads a BE-encoded low u64 ID address from an u256 word, interprets it as an actor ID, and looks up the f4 address, returning an empty array if not found.

Input data layout

<actor id> u64 (right padded to fit u256)

Return data layout

<address> as bytes

Or empty if error, or inexistent.

call_actor precompile

Calls an actor with the ability to supply a method number, using any Filecoin address.

Input data layout

<method number> as u64 (left padded to u256)
  || <value> as u256
  || <send flags> as u64 (left padded to u256)
  || <input data codec> as u64 (left padded to u256)
  || <input data offset> as u32 (left padded to u256)
  || <callee address offset> as u64 (left padded to u256)

at <input data offset>:
<input data length> as u32 (left padded to u256)
  || <input data> as bytes

at <callee address offset>:
<callee address length> as u32 (left padded to u256)
  || <callee address> as bytes
uint64 method;
uint256 value;
uint64 flags;
uint64 codec;
bytes memory params;
bytes memory target_address;
abi.encode(method, value, flags, codec, params, target_address)

Return data layout

<exit code> as u32 (left padded to u256)
  || <codec> as u64 (left padded to u256)
  || <return data offset> as u32 (left padded to u256)

at <return data offset>:
<return data length> as u32 (left padded to u256)
  || <return data> as bytes
  || optional 0 padding to round out an Ethereum word
abi.decode(data, (int256, uint64, bytes));

call_actor_id precompile

Calls an actor with the ability to supply a method number, using an actor ID.

Input data layout

<method number> as u64 (left padded to u256)
  || <value> as u256
  || <send flags> as u64 (left padded to u256)
  || <input data codec> as u64 (left padded to u256)
  || <input data offset> as u32 (left padded to u256)
  || <actor id> as u64 (left padded to u256)

at <input data offset>:
<input data length> as u32 (left padded to u256)
  || <input data> as bytes
uint64 method;
uint256 value;
uint64 flags;
uint64 codec;
bytes memory params;
uint64 actor_id
abi.encode(method, value, flags, codec, params, actor_id)

Return data layout

<exit code> as u32 (left padded to u256)
  || <codec> as u64 (left padded to u256)
  || <return data offset> as u32 (left padded to u256)

at <return data offset>:
<return data length> as u32 (left padded to u256)
  || <return data> as bytes
  || optional 0 padding to round out an Ethereum word
abi.decode(data, (int256, uint64, bytes));

Other considerations

Historical support

Historical Ethereum behaviors are not supported. EVM opcode and precompile support is restricted to the Ethereum Paris fork, plus PUSH0 from EIP-3855.

Transaction types

The only supported Ethereum transaction type is the EIP-1559 transaction (type 2 in the RLP-encoded transaction format). Such transactions carry a gas fee cap and a gas premium, both of which map cleanly to Filecoin’s message model.

Native currency

The native currency of the Filecoin EVM runtime is filecoin. This environment has no dependence to, or understanding of, Ether as a currency.

Specification: Filecoin Virtual Machine changes

Environmental data during FVM Machine construction

New environmental data is required by the FVM to satisfy new data returned in syscalls:

  • [EIP-155] Chain ID of the network.
  • Timestamp of the execution epoch. If the execution epoch happens to be a null round (no blocks, and therefore no tipset), the client must pass the timestamp of the epoch at which the null round occurred (i.e. the timestamp that a block would’ve carried should that epoch not have been a null round).

EIP-155 Chain IDs of Filecoin networks

We define the following EIP-155 Chain IDs for Filecoin networks:

  • Mainnet: 314
  • Hyperspace testnet: 3141
  • Wallaby testnet: 31415
  • Calibration testnet: 314159
  • Butterfly testnet: 3141592
  • Local/private testnets: 31415926

These Chain IDs have been registered in the ethereum-lists/chains registry, which in turn is prepared for CAIP-2 compliance.

Added syscalls

network::context

#[repr(packed, C)]
pub struct NetworkContext {
   /// The current epoch.
   pub epoch: ChainEpoch,
   /// The current time (seconds since the unix epoch).
   pub timestamp: u64,
   /// The current base-fee.
   pub base_fee: TokenAmount,
   /// The Chain ID of the network.
   pub chain_id: u64,
   /// The network version.
   pub network_version: u32,
}

/// Returns the details about the network.
///
/// # Errors
///
/// None
pub fn context() -> Result<NetworkContext>;

This syscall charges no gas beyond the base p_syscall_gas.

network::tipset_cid

See Tipset CID for more context.

/// Retrieves a tipset's CID within the last finality, if available.
///
/// # Arguments
///
/// - `epoch` the epoch being queried.
/// - `ret_off` and `ret_len` specify the location and length of the buffer into which the
///   tipset CID will be written.
///
/// # Returns
///
/// Returns the length of the CID written to the output buffer.
///
/// # Errors
///
/// | Error               | Reason                                       |
/// |---------------------|----------------------------------------------|
/// | [`IllegalArgument`] | specified epoch is negative or in the future |
/// | [`LimitExceeded`]   | specified epoch exceeds finality             |
pub fn tipset_cid(
   epoch: i64,
   ret_off: *mut u8,
   ret_len: u32,
) -> Result<u32>;

The lookback is limited to [current_epoch - 900, current_epoch - 1].

In addition to the base p_syscall_gas, this syscall charges:

  1. A flat 50,000 gas fee to lookup the tipset CID at the previous epoch.
  2. A flat 215,000 gas fee to lookup the tipset CID at any other epoch.

actor::lookup_address

/// Looks up the "delegated" (f4) address of the target actor (if any).
///
/// # Arguments
///
/// `addr_buf_off` and `addr_buf_len` specify the location and length of the output buffer in
/// which to store the address.
///
/// # Returns
///
/// The length of the address written to the output buffer, or 0 if the target actor has no
/// delegated (f4) address.
///
/// # Errors
///
/// | Error               | Reason                                                           |
/// |---------------------|------------------------------------------------------------------|
/// | [`NotFound`]        | if the target actor does not exist                               |
/// | [`BufferTooSmall`]  | if the output buffer isn't large enough to fit the address       |
/// | [`IllegalArgument`] | if the output buffer isn't valid, in memory, etc.                |
pub fn lookup_delegated_address(
   actor_id: u64,
   addr_buf_off: *mut u8,
   addr_buf_len: u32,
) -> Result<u32>;

This syscall charges to lookup the target actor state (see FIP-0057), in addition to the base p_syscall_gas

actor::balance_of

/// Gets the balance of the specified actor.
///
/// # Arguments
///
/// - `actor_id` is the ID of the target actor.
///
/// # Errors
///
/// | Error                | Reason                                         |
/// |----------------------|------------------------------------------------|
/// | [`NotFound`]         | the target actor does not exist                |
pub fn balance_of(
   actor_id: u64
)  -> Result<super::TokenAmount>;

This syscall charges to lookup the target actor state (see FIP-0057), in addition to the base p_syscall_gas

actor::next_actor_address

/// Generates a new actor address for an actor deployed by the calling actor.
///
/// **Privileged:** May only be called by the init actor.
pub fn next_actor_address(obuf_off: *mut u8, obuf_len: u32) -> Result<u32>;

This syscall charges no gas beyond the base p_syscall_gas.

crypto::recover_secp_public_key

/// Recovers the signer public key from a signed message hash and its signature.
///
/// Returns the public key in uncompressed 65 bytes form.
///
/// # Arguments
///
/// - `hash_off` specify location of a 32-byte message hash.
/// - `sig_off` specify location of a 65-byte signature.
///
/// # Errors
///
/// | Error               | Reason                                               |
/// |---------------------|------------------------------------------------------|
/// | [`IllegalArgument`] | signature or hash buffers are invalid                |
pub fn recover_secp_public_key(
   hash_off: *const u8,
   sig_off: *const u8,
) -> Result<[u8; SECP_PUB_LEN]>;

This syscall charges 1,637,292 gas (in addition to the base syscall gas), the same as is charged to verify a secp256k1 signature.

event::emit_event

Originally defined in FIP-0049 (Actor events). Redefined and bundled here for convenience.

/// Emits an actor event to be recorded in the receipt.
///
/// Expects a DAG-CBOR representation of the ActorEvent struct.
///
/// # Errors
///
/// | Error               | Reason                                                              |
/// |---------------------|---------------------------------------------------------------------|
/// | [`IllegalArgument`] | entries failed to validate due to improper encoding or invalid data |
pub fn emit_event(
   evt_off: *const u8,
   evt_len: u32,
) -> Result<()>;

gas::available

/// Returns the amount of gas remaining.
pub fn available() -> Result<u64>;

This syscall charges no gas beyond the base p_syscall_gas.

vm::exit

/// Abort execution with the given code and optional message and data for the return value.
/// The code and return value are recorded in the receipt, the message is for debugging only.
///
/// # Arguments
///
/// - `code` is the `ExitCode` to abort with.
///   If this code is zero, then the exit indicates a successful non-local return from
///   the current execution context.
///   If this code is not zero and less than the minimum "user" exit code, it will be replaced with
///   `SYS_ILLEGAL_EXIT_CODE`.
/// - `blk_id` is the optional data block id; it should be 0 if there are no data attached to
///   this exit.
/// - `message_off` and `message_len` specify the offset and length (in wasm memory) of an
///   optional debug message associated with this abort. These parameters may be null/0 and will
///   be ignored if invalid.
///
/// # Errors
///
/// None. This function doesn't return.
pub fn exit(code: u32, blk_id: u32, message_off: *const u8, message_len: u32) -> !;

This syscall charges no gas beyond the base p_syscall_gas.

vm::message_context

#[repr(packed, C)]
pub struct MessageContext {
   /// The current call's origin actor ID.
   pub origin: ActorID,
   /// The caller's actor ID.
   pub caller: ActorID,
   /// The receiver's actor ID (i.e. ourselves).
   pub receiver: ActorID,
   /// The method number from the message.
   pub method_number: MethodNum,
   /// The value that was received.
   pub value_received: TokenAmount,
   /// The current gas premium
   pub gas_premium: TokenAmount,
   /// The current gas limit
   pub gas_limit: u64,
   /// Flags pertaining to the currently executing actor's invocation context.
   /// Where ContextFlags is an u64-encoded bitmap, accepting values:
   /// - 0x01: read only
   pub flags: ContextFlags,
}

/// Returns the details about the message causing this invocation.
///
/// # Errors
///
/// None
pub fn message_context() -> Result<MessageContext>;

This syscall charges no gas beyond the base p_syscall_gas.

Changed syscalls

send::send syscall

This syscall now takes two extra fields:

  • gas_limit (u64), the gas limit applicable to the send (where u64::MAX can be safely used to mean “no limit”). This value is always clamped to the maximum gas available.
  • send_flags (u64), encoding a bitmap of send flags.

The possible send flags are:

  • 0x01: Read-only. Nor the callee nor any of its transitive callees can cause any side effects: state updates, value transfers, or events. Specifically, the following calls all fail with the ReadOnly error number:
    • self::self_destruct
    • self::set_root
    • event::emit_event
    • send::send when transferring a non-zero balance or auto-creating the recipient.
    • actor::create_actor. This causes Init::Exec and Init::Exec4 to exit with USR_READ_ONLY (see New general exit codes).

This syscall now returns a new error number:

  • ReadOnly (number 13): returned when the called actor tries to perform one of the restricted mutations.

crypto::hash syscall

This syscall now supports the following hashes, specified by their multicodec value:

  • Sha2_256 (0x12)
  • Blake2b256 (0xb220)
  • Blake2b512 (0xb240)
  • Keccak256 (0x1b)
  • Ripemd160 (0x1053)

Other syscall changes

  • vm::abort is replaced by the more general syscall vm::exit, which can accept a zero exit code, as well as return data on error.
  • vm::context is renamed to vm::mesage_context.
  • network::base_fee is superseded by network::context, which returns the base fee, in addition to other fields.
  • actor::new_actor_address is replaced by actor::next_actor_address.

New externs

We define a new extern, a function provided by the host client, to retrieve the Tipset CID of a given epoch.

/// Gets the CID for a given tipset.
///
/// If the supplied epoch happens to be a null round, the client must return the CID
/// of the previous first non-null round.
fn get_tipset_cid(&self, epoch: ChainEpoch) -> anyhow::Result<Cid>;

New general exit codes

  • USR_READ_ONLY (exit code 25). To be returned by actors when the requested operation cannot be performed in “read-only” mode. Specifically:
    • Value transfers in send::send.
    • Actor state-root changes (self::set_root).
    • self::selfdestruct
    • Actor creation.

Specification: Client changes

Tipset CID

We introduce the concept of the tipset CID, which uniquely identifies the set of blocks that were incorporated to the blockchain at a given epoch.

In the past, tipsets were identified by a composite key enumerating the block CIDs of every block in canonical order, also known as “tipset key”.

The tipset key is the concatenation of the bytes of the CIDs of the blocks that make up the tipset, in lexicographic order of their Ticket field (block canonical order). It is CBOR-serialized as a byte string (major type 2).

Starting from now, clients must compute and remember the tipset CID of every tipset committed to the chain. This is achieved by inserting the CBOR-serialized tipset key into the chain blockstore, and computing its CID using the Blake2b-256 multihash function.

Design Rationale

Tipset CID Gas

The Tipset CID computation gas was determined by benchmarking Lotus and adding a security factor, but the numbers should hold for most implementations. Specifically, the benchmark assumes that:

  1. The last 900 tipsets are cached in-memory.
  2. There exists a skip-list allowing one to skip 20 tipsets at a time.

This true for all major Filecoin clients: Forest, Venus, and Lotus.

Benchmarking yielded:

  • ~1,500 gas/20 tipsets (skip list)
  • ~5,800 gas to traverse each tipset directly.
  • ~15,000 gas offset (includes overhead, computing the tipset CID, etc.).

Purely for the client-side operations. We then add on an additional 21,000 gas (p_extern_gas) to charge for calling into the client.

To keep the common case of getting the last tipset CID (e.g., for randomness) affordable, we split this charge into:

  1. The most recent tipset CID lookup: 15000+5800+21000 = 41800. We propose 50,000 for security.
  2. Everything else: (900/20)*1500 + 5800*19 + 15000 + 21000 = 213700. We propose 215,000 for security.

These charges intentionally leave space for changing the underlying tipset caching algorithm.

Transfer Gas Limit

The EVM has a concept called the gas “stipend”. Every call with a non-zero value transfer is granted 2300 gas, automatically. Solidity further extended this concept by introducing special send and transfer functions that explicitly apply this stipend to zero-value transfers.

Unfortunately, in FEVM, 2300 gas isn’t enough to do anything due to the differences in the gas model. To ensure that these functions actually work, the FVM detects this case and sets the gas limit to 10M, which should be more than enough gas to:

  1. Lookup an address.
  2. Create a placeholder actor, if necessary.
  3. Transfer funds, persisting both the sender and recipient’s states.
  4. Launch the EVM actor and run a bit of code.

All together, this should cost no more than ~6-7M gas where the majority of that gas accounts for state read costs. These state-read costs are not expected to increase in the future.

We discarded the following alternatives:

  1. Keep the gas limit as specified by the caller. This would have broken send and transfer, so it was never really an option.
  2. Set the gas limit to “unlimited” (or, really, 63/64 of the remaining gas). This would have made the transfer “work”, but it would have introduced a potential security issue: any contracts relying on send terminating after spending a reasonable amount of gas would be broken. This could have been used to get contracts into “stuck” states. The correct fix for this is to use the “Withdrawal” pattern, but not all contracts are written correctly.
  3. Avoid running code when this gas limit is detected (e.g., by using method 0). This option was discarded as some dapps rely on contracts being able to emit events when they receive funds.
  4. Set a precise limit. Any precise limits would have required adjusting the limit over time as network gas costs changed.

Flat vs. nested contract deployment model

Early on, we had to choose the broad architecture by which EVM smart contracts would become an entity on-chain. We considered two large architectural options:

  1. A flat deployment model, where each EVM smart contract is an independent actor on-chain, with its own entry in the state tree, and its own address set.
  2. A nested deployment model, where a singleton built-in actor would contain the entirety of the EVM domain within it, with some form of internal fragmentation to model every smart contract as an independent entity within a monolithic state.

The choice went hand-in-hand with the addressing model, since we needed to assign and recognize Ethereum addresses either way. With (1), we’d need to find a way to delegate addressing to another actor that controlled a discretionary address space. With (2), addressing could be recursive, where the address first referred to the singleton actor, followed by an opaque component to be interpreted by such actor for internal routing.

This choice is a foundational one, as it defines the architectural trajectory of the Filecoin network for years to come, insofar programmability is concerned. These are the main reasons we chose to adopt (1):

  1. It places EVM smart contracts, as well as any other future foreign program, at equal footing and hierarchical level as any other actor, such as built-in actors, eventual user-deployed Wasm actors, and other future entities.
  2. It physically isolates state between actors through a shared-nothing model. This simplifies state management, makes state more granular, prevents undesired churn on update, eliminates the possibility of side effects, and contagion of IO overheads across smart contracts.
  3. It enables future versioning of the EVM runtime with explicit opt-in from developers at the contract level (i.e. optionality).
  4. It forces us to introduce general protocol advancements to facilitate seamless interoperability across supported runtimes, versus hyperspecialization within runtimes.

See the discussion under filecoin-project/ref-fvm#742 for further context.

Optimizing EVM storage

The EVM contract storage model makes no assumptions about concrete key patterns, slot placement of state variables, nor slot addressing in the case of complex and/or dynamic types such as structs, maps, or arrays. Languages such as Solidity and Vyper are responsible mapping state to EVM storage by adopting a concrete storage layout for its types. When designing the KAMT, we optimized for the Solidity storage layout, such that:

  1. Contiguous slots are adjacently placed within the same node as adjacent pointers, or in adjacent nodes if overflowing.
  2. Access to slots sharing common prefixes is shortcut through extensions that skip otherwise-sparse tree levels.

Optimizing for Solidity makes sense because it is, by far, the most popular Ethereum smart contract programming language. Furthermore, Vyper has adopted Solidity’s storage layout for complex and dynamic state types.

Value sends

Ethereum does not distinguish between bare value sends and contract invocations. Bare value sends can trigger smart contract logic in Ethereum.

Filecoin distinguishes one from the other through the method number. Method number 0 designates a bare value send and does not dispatch the call to actor logic on the recipient. Method numbers other than 0 result in a Wasm actor instantiation and the consequent call dispatch.

We have not altered these mechanisms in this FIP. As a result, bare value sends to EVM smart contracts will not trigger EVM smart contract logic. This is a significant functional difference between Filecoin and Ethereum.

Refer to the community discussion at filecoin-project/ref-fvm#835 for more context.

FRC42 method number of InvokeContract`

This method has been assigned a number in the non-reserved range compliant with the [FRC42 call convention] to pave the way for future Wasm actors to be able to act as Filecoin EVM smart contracts by, for example, processing native ERC-20, ERC-721, etc. transactions submitted from Ethereum wallets. Having chosen a reserved number, e.g. 2, would’ve raised the risk of method number conflicts down the line.

Backwards Compatibility

Built-in actors must be updated to absorb the syscall changes specified herein to retain backwards compatibility. This will lead to a new version of actors being produced. A migration is required to add their Wasm bytecode to the blockstore, to link in their CodeCIDs in the System actor, and to update all existing actors to the new CodeCIDs.

Furthermore, changes in syscalls must be absorbed by other built-in actors to preserve backwards compatibility.

Test Cases

See integration tests in filecoin-project/ref-fvm, as well as unit and integration tests of the EVM runtime actor.

Security Considerations

This FIP enables developers to deploy custom, untrusted code to the Filecoin network for the first time. The security considerations are multi-faceted:

  1. Value will no longer flow exclusively through trusted smart contract logic. Users must exercise caution and carefully analyze and decide what and who to trust.
  2. EVM smart contract execution is doubly sandboxed. First, within the Filecoin EVM runtime actor; and second within the Wasm invocation container. Thus, the risk of malicious systemic outcomes is contained.
  3. Inside the current gas architecture, the popularity of user contracts may conflict with the need for storage providers to send system messages to continue operating and managing the network. It is hard or impossible to forecast how the gas market will be impacted by the arrival of user-defined contracts to Filecoin. The FVM team has requested the Cryptoecon Lab to produce a community report to evaluate possible scenarios for preparedness.

Incentive Considerations

This is a technical FIP that independently raises no incentive considerations. However, smart contracts may introduce new incentive structures to the Filecoin network that users could engage with.

Product Considerations

Product considerations are broken down into subsections covering various topics.

Gas

Gas metering and execution halt are performed according to the Filecoin gas model. EVM smart contracts accrue:

  • Execution costs: resulting from the handling of Wasm instructions executed during the interpretation of the EVM bytecode, as well as the EVM runtime actor logic (e.g. method dispatch, payload handling, and more).
  • Syscall and extern costs: resulting from the syscalls made by the EVM opcode handlers, and the EVM runtime actor logic itself.
  • Memory expansion costs: resulting from the allocation of Wasm memory pages.
  • Storage costs: resulting from IPLD state reads and accesses, directly by the smart contract as a result of SSTORE and SLOAD opcodes, or indirectly by the EVM runtime actor during dispatch or opcode handling.

As specified above, gas-related opcodes such as GAS and GASLIMIT return Filecoin gas, coercing their natural u64 type to u256 (EVM type). The gas limit supplied to the CALL, DELEGATECALL and STATICCALL opcodes is also Filecoin gas.

Furthermore, gas limits specified when calling precompiles are disregarded.

Consequently, contracts ported from Ethereum that use literal gas values (e.g. the well known 2300 gas price for bare value transfers) may require adaptation, as these gas values won’t directly translate into the Filecoin gas model.

Gas changes through upgrades

The EVM runtime actor backing EVM smart contracts may be upgraded through future migrations. Upgrades will impact gas costs in hard-to-predict ways, with compute gas being the most sensitive component. We discourage smart contract developers to rely on specific gas values in their contract logic. This includes gas limits passed in *CALL* operations. While the system honors those limits, costs will change across upgrades, so it’s unsafe to hardcode assumptions.

Main differences between Filecoin’s EVM and Ethereum’s EVM

  1. Gas metering and execution halt are performed according to the Filecoin gas model. Ethereum gas accounting does not apply in the Filecoin EVM.
  2. Gas limits set when CALLing precompiles do not apply, except for the call_* precompiles where the gas limit supplied to the CALL constrains the resulting send.
  3. Filecoin gas costs are not stable over network updates. Developers must not make gas usage assumptions, as Filecoin gas is highly sensitive to implementation details, and they should refrain from using hardcoded gas limits in calls.
  4. Bare value sends (messages with method number 0) to Filecoin EVM smart contracts do not trigger smart contract logic, even if the message carries parameters.
  5. The CALLCODE opcode is not supported, since it was superseded in Ethereum by the more robust DELEGATECALL opcode, there are requests to deprecate, and Solidity no longer supports it as of v0.5.0.
  6. SELFDESTRUCT behavior:
    • Contrary to Ethereum, if a self-destructed contract gets sent funds after it calls SELFDESTRUCT but before the transaction ends, those funds don’t vanish. Instead, they remain in the tombstoned smart contract.
    • Contrary to Ethereum, SELFDESTRUCT does not trigger a physical deletion of the contract, but rather a logical deletion by way of tombstoning. Thus, there is no gas refund for deleting an EVM smart contract.

Inability to stably address some actors from EVM smart contracts

Only Ethereum Accounts and EVM smart contracts are assigned an f410 address. See FIP-0055 for more context. All other actors need to be addressed through Masked ID addresses.

Unfortunately, contrary to f410 addresses, ID addresses are not reorg stable. In other words, the ID may be reassigned within the chain’s finality window.

This means that only these interactions benefit from addressing stability within the 900 epochs of the creation of the callee actor, when being addressed through their Ethereum address from within an EVM smart contract:

  1. EVM <> EVM interactions.
  2. EVM <> EthAccount interactions.

Calls from EVM to non-EVM actors must used Masked ID addresses, which do not benefit from stability until after 900 epochs from creation. This means that the ID address may be reassigned within 900 epochs from creation.

EVM smart contracts can only address the following actors through Masked ID addresses, thus making these interactions subject to recency instability:

  1. Non-Ethereum accounts (using f1/f3 addresses is not possible from within EVM smart contracts).
  2. Other built-in actors like the miner actor, multisigs, etc.

The stability issue is not a problem for singleton actors (e.g. power actor, storage market actor, etc.), as these actors sit at fixed ID addresses.

Stemming from this, transferring value to inexistent f1 and f3 addresses from EVM smart contracts is not possible. These are counterfactual interactions where the target still doesn’t have an ID address, and thus cannot be currently addressed from an EVM smart contract.

Note that we discarded assigning f410 addresses to non-Ethereum related actors at this stage in the name of simplicity, but we may revisit this decision in the future, depending on user feedback.

Implementation

At the time of writing, the EVM runtime actor implementation resides in the next branch of ref-fvm.

Copyright and related rights waived via CC0.

  1. FRC42 hash of InvokeEVM 2

Citation

Please cite this document as:

Raúl Kripalani, Steven Allen, "FIP-0054: Filecoin EVM runtime (FEVM)," Filecoin Improvement Proposals, no. 0054, December 2022. [Online serial]. Available: https://fips.filecoin.io/fips/fip-0054.