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

FIP0054: Simplify and expand CALL logic #607

Merged
merged 3 commits into from
Feb 3, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 48 additions & 21 deletions FIPS/fip-0054.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub struct State {
pub bytecode: Cid,

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

/// The EVM contract state dictionary.
/// All EVM contract state is a map of U256 -> U256 values.
Expand Down Expand Up @@ -221,7 +221,7 @@ The Constructor runs the init bytecode with the EVM interpreter, and populates t
- `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](#kamt-specification) resulting from `SSTORE` operations during construction.
- `nonce` field: set to 0, unless any calls to CREATE or CREATE2 happen during initialization.
- `bytecode_hash`: the last 20 bytes of the Keccak-256 hash of the bytecode.
- `bytecode_hash`: the Keccak-256 hash of the bytecode.
- `tombstone`: may be non-empty if the contract selfdestructed itself during construction.

_Input parameters_
Expand Down Expand Up @@ -575,38 +575,45 @@ The contract isn't considered to be "dead" until it has a tombstone where the re
- Calls to this contract in subsequent transactions will return successfully with no code being executed.

**Calls: `CALL`.**
Performs a call to another actor, behaving differently depending on the target's actor type.
If the target address is a precompile address, it is handled internally by first calling the precompile logic, and then transferring any value to the actual precompile address on-chain.
Otherwise, the target actor type is determined by calling the `actor::get_actor_code_cid` and `actor::get_builtin_actor_type` syscalls.
If the target is another EVM smart contract or a non-account Wasm actor, it invokes its `InvokeContract` method.
If the target is a placeholder, an account, or an EthAccount actor ([FIP-0055]), it lowers the call to a bare value send.
See [Value sends](#value-sends) for considerations.

If the gas limit is 2300, no matter the target's type, the call is lowered to a bare value send.
This is to honor the well-known Ethereum convention of setting a gas limit equal to 2300 to prevent any possible contract logic from running.
First, if the recipient is a _precompile_, FEVM invokes the precompile directly as follows:

The 1/64th gas reservation rule from Ethereum is honored.
CALL opcodes specifying no gas limit will implicitly set a 63/64th gas limit for the callee, except when the callee is a precompile (since precompiles are resolved internally).
Note that the reservation is applied on Filecoin gas, not Ethereum gas.
See [Product considerations: Gas](#gas) for more information.
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](#addressing) (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](#addressing).
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](#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.
Copy link
Member

Choose a reason for hiding this comment

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

If we're finally going in this direction (static translation of gas limit), it would make sense to add a point in Design rationale about having to maintain this equivalent eternally, as:

a) the gas model evolves release over release
b) the EVM runtime evolves and adds/removes overhead

Copy link
Member Author

Choose a reason for hiding this comment

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

it would make sense to add a point in Design rationale about having to maintain this equivalent eternally, as

I'll expand on it. The goal is to pick a number such that that it will never be an issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm also happy to pick a higher number, like 100M. That would be 1% of the maximum block size.

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](#gas) for more information.

**Calls: `CALLCODE`.**
Not supported.
Aborts with `EVM_CONTRACT_EXECUTION_ERROR` (exit code 34) when used.
Aborts with `EVM_CONTRACT_UNDEFINED_INSTRUCTION` (exit code 36) when used.
This is because [EIP-7](https://eips.ethereum.org/EIPS/eip-7) introduced `DELEGATECALL` as a hardened replacement for `CALLCODE`.
Solidity no longer supports `CALLCODE`.

**Calls: `DELEGATECALL`.**
Fetches the bytecode of the target, as long as it's an EVM runtime actor, 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.
If the target is another Wasm actor, we revert (aborting with exit code 33, i.e. `EVM_CONTRACT_REVERTED`).
If the target is a precompile address, it is handled internally by first calling the precompile logic, and then transferring any value to the actual precompile address on-chain.

A delegate call against a precompile has the same effect as `CALL`.
`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.
See [`send::send`](#sendsend-syscall) for more information.
Specifically, it sets the "read-only" bit in the send flags (bit `0x1`). See [`send::send`](#sendsend-syscall) for more information.

**External code: `EXTCODESIZE`.**

Expand Down Expand Up @@ -1132,6 +1139,26 @@ This is achieved by inserting the CBOR-serialized tipset key into the chain bloc

## Design Rationale

### 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.
Expand Down