By @JonahB, @Maddiaa0 & @NFTtaylor
Thanks to those @blockchaincap who gave feedback on the article.

A blockchain’s core product is its virtual machine—everything else exists to keep that engine running smoothly. In this article, we’ll compare Ethereum’s Virtual Machine (EVM) to Solana’s Virtual Machine (SVM). By understanding how each VM is built, it becomes clear why staking models, fee markets, and networking all diverge so dramatically.

EVM

Runtime architecture

In Ethereum’s runtime, the virtual machine is a single-threaded stack-based VM that operates on 256 bit integers. This was chosen because the EVM makes prolific use of 256-bit cryptography. The EVM maintains a custom list of opcodes, with each opcode mapped to a cost in gas (we will cover Ethereum and Solana fee mechanics in upcoming posts). During EVM code execution, the VM maintains memory as a byte array. Memory is ephemeral and does not persist after the transaction. Ethereum maintains persisted world state in a modified Merkle tree named the “Merkle Patricia Trie”.

Contract model

On Ethereum, Smart Contracts are immutable and deployed at an address derived from the deployer’s address and their transaction nonce, or via the create (create / create2) opcodes. Each smart contract maintains it’s own state, called storage, that is stored as key/value where each key is a full word (32 bytes) and each value is also 32 bytes. Critically, each contract's storage is name-spaced and segregated from all other contracts. During execution, a contract can only read and mutate state from within it's own storage space (limiting reads to the current contract's state is one key differentiator from the SVM). Smart contract controlled state allows for rich, stateful interactions simply by calling a function directly on the smart contract.

ABI and developer ergonomics

What makes the EVM so simple for developers to onboard to is the extremely opinionated nature of the ecosystem that has been built around it. The ABI standard, first introduced to the ecosystem via serpent. Formalizes the wire format of Ethereum messages, allowing both users and contracts to interact with each other through only interfaces.

// ERC20.sol

// State for storing user balances, keyed by an address
mapping(address account => uint256) private _balances;

// Integeger state tracking total supply of the token
uint256 private _totalSupply;

// Token metadata
string private _name;
string private _symbol;

The ABI defines function selectors, a 4 byte identifier used to locate and execute the corresponding logic (under the hood it just adds an entry to a jump table that routes to the function's runtime code). Function selectors are the first 4 bytes of the keccak256 hash of the function name. In Solidity, the primary language used for writing EVM smart contracts, there are multiple visibility levels for functions. Marking a function as public exposes the function selector, while marking as internal does not. However, in Solidity not all public functions are the same. Given each contract maintains it’s own internal state, view functions exist for the developer to expose state or computed state. View functions can only query contract state and cannot modify it. Functions marked as pure may perform calculations or transformations without accessing or modifying contract state. Functions without these modifiers may mutate contract state that will be persisted to the global Merkle Patricia Trie.

// ERC20.sol

/**
 * Example of a *view* function that exposes the name of the token.
 */
function name() public view virtual returns (string memory) {
    return _name;
}

/**
 * Example of an internal function that moves a `value` amount of tokens from `from` to `to`.
 */
function _transfer(address from, address to, uint256 value) internal {
    if (from == address(0)) {
        revert ERC20InvalidSender(address(0));
    }
    if (to == address(0)) {
        revert ERC20InvalidReceiver(address(0));
    }
    _update(from, to, value);
}

/**
 * Example of a function that is callable by external addresses.
 */
function transfer(address to, uint256 value) public virtual returns (bool) {
    address owner = _msgSender();
    _transfer(owner, to, value);
    return true;
}

Inter‑contract composability

Smart contract interoperability in the EVM enables synchronous calls between contracts within a single transaction, using internal function calls or external message calls. Leveraging the shared global state, contracts can invoke functions, pass data, or transfer Ether in the same execution context, enhancing composability. ERC interfaces, such as ERC-20, standardize function signatures and events, allowing contracts to interact seamlessly with any compliant contract without knowing its implementation details.

SVM

eBPF core and Sealevel scheduler

Solana's VM utilizes eBPF (extended Berkley Packet Filter) virtual machine, a register-based system that already exists within all major linux distributions, and thus had been heavily optimized for many years before Solana came along. The most critical difference from Solana to Ethereum is it's transaction scheduler. The Sealevel Runtime allows for concurrent processing of non-conflicting transactions, significantly boosting throughput. It is not necessarily the vm itself that leads to parallelization, but the runtime itself. This is reaffirmed by a recent research paper from LayerZero, where efficient scheduling, not necessarily VM optimization, leads to huge performance improvements.

Compute budget and fee model

Unlike Ethereum, where every opcode triggers differing amounts of gas depending on what type of instruction is used, Solana charges the same amount for each eBPF opcode, execution is capped via a similar mechanism, where there is a fixed max compute budget per transaction, first set by the user, however there are also protocol enforced limits.

State models

Unlike Ethereum, which namespaces contract storage so only the owning smart contract can read or write to it, Solana takes a different approach.

Ethereum state tree (for comparison)

Below is a (very simplified) version of what the Ethereum Accounts tree roughly looks like. Contracts and EOA's share the same data structure, Contracts have code set and an accompanying contract state trie. The implementation details of the Merkle Patricia Trie are left out of the below diagrams, but for those looking to learn more see here.

Both Contract (Code Accounts) and EOA’s (Externally Owned Accounts / Signing Accounts) share the same data structures. EOA’s just do not have a codeHash or stateHash set.

Here is a diagram pulling all of it together:

Solana state tree:

In Solana, State, Programs and Accounts all exist within the same address space, creating a much flatter storage structure. Below is an (equally simplified) example of the Solana State Tree:

State, including executable programs, are all stored in separate accounts with unique addresses. Accounts are managed by a built-in program called the System Program by default and have a size limit of 10MiB. Program accounts are marked as executable to indicate that their data is eBPF bytecode. Unlike in the EVM, programs in Solana do not have their own name-spaced state (their own state tree) as their account only stores the executable. As data is stored in the same address space as programs and accounts.

All entries have an "owner" field assigned to their address, which grants the permission to write data to the account. So rather than specifying state in the smart contract, SVM programs will contain data structures that represent different account types to be owned by the program. Proper account management is critical for ensuring a program does not break the possibility of parallel transaction execution. For example, two transactions touching the same AMM pool will not be able to be executed at the same time, as they will touch the same state.

One key difference of not scoping state to a particular contract, is that Solana contracts have the ability to read state from any other account, as they all share the same address space.

Instruction dispatch patterns

In Solidity / EVM, if a developer wants to read the balance of a token for a particular account, they would call the balanceOf(address) getter on the ERC20 interface. In contrast, Solana allows direct access to the raw bytes stored at a given account address, eliminating the need for such a call.

This is not too much of a deviation from the EVM as this exact functionality can be replicated using the extcode opcode. It makes the contract code of another account readable! Imagine being able to store arbitrary state in another contract that can be read from (this is the technique utilized by sstore2, a popular library). The problem in the EVM is that there is no clean way to update the data stored in an account, as discussed Solana solves this with the "owner" field.

To underscore this point: all data in Solana lives in a single flat tree—contract storage, EOAs, and programs all share the same structure. This goes a step beyond Ethereum, where EOAs and contracts exist in the same accounts tree, but contract state is kept separate.

One consequence of state being accessible via a public address in the SVM is that view functions, which are essential in EVM smart contracts, become unnecessary. Rather, developers deserialize the raw data in accounts to read program specific state. This reduces the amount of smart contract code, while putting the burden on the client developer. Developers could write view functions and use simulate RPC calls on Solana nodes, just as Ethereum developer's often do. But since Uniswap on Solana would not need the balanceOf() function in order to read somebody's balance (it can just query the Program storage directly in current vm context). Developers do not feel the need to include them.

Frameworks and ABI‑like conventions

Unlike EVM smart contracts where the popular tooling has centered around ABI based function dispatching, Solana does not enforce any such patterns for executable program design. Rather, the developer of the program determines how to route instructions that call the program typically by reading the instruction data. Similarly to the ABI, by default, the popular Anchor framework uses the first 8 bytes of a hash of the instruction name. But developers not using frameworks can choose how to execute logic in any manner they see fit.

// Program entrypoint in a non-Anchor program
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
        // Read the first byte of instruction data
    let (discriminator, instruction_data) = instruction_data
        .split_first()
        .ok_or(ProgramError::InvalidInstructionData)?;
        // Route what instruction (aka logic) to execute
    match discriminator {
        0 => process_initialize(program_id, accounts, instruction_data),
        1 => process_transfer(program_id, accounts, instruction_data),
        99 => process_random(program_id, accounts, instruction_data),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

In the EVM world, this would be equivalent to writing a Huff contract that reads the first byte from calldata, then jumps to the desired function's implementation:

#define macro MAIN() = {
  // place first 8 bytes of calldata on the stack
  0x00 calldataload 0xc0 shr

  // The following section is functionally a switch statement
  // switch (calldata[0x00:0x01]) {
  //     case 0x00: process_initialize();
  //     case 0x01: process_transfer();
  //     case 0x63: process_random();
  //     default: revert();
  // }
  dup1 0x00 eq process_initialise jumpi
  dup1 0x01 eq process_transfer   jumpi
  dup1 0x64 eq process_random     jumpi

  // revert takes in memory_pointer, and size - set both to 0
  0x00 0x00 revert

    process_initialise:
        PROCESS_INITIALIZE()
    process_transfer:
        PROCESS_TRANSFER()
    process_random:
        PROCESS_RANDOM()
}

Anchor as a framework frees developers from these implementation decisions, much like how the ABI's integration into popular Ethereum compilers means users don't need to worry about these details. If Ethereum had chosen LLVM as its default developer tooling (with a Rust frontend), developers would face similar choices—at least until a framework enforcing the ABI emerged.

Program Derived Addresses (PDAs)

In the early days of Solana, all accounts owned by programs were externally owned keypairs that had been re-assigned from the System Program to a deployed program. This was problematic because all accounts were nondeterministic and programs were unable to sign on behalf of these accounts. PDAs we’re introduced to resolve these issues. PDAs are addresses off the Ed25519 curve, so they cannot clash with user owned addresses, and are derived from a list of pre-determined seeds. A deep dive on PDAs is outside the scope of this article, but it’s worth noting that program owned deterministic addresses allows for more comprehensive program state management as PDAs can act as hashmap like structures.

Cross‑Program Invocations (CPIs)

CPIs in Solana allow one program to call another program’s functions, within a single instruction. These invocations require explicit passing of account references, which must be passed through from the top level transaction. If the instruction being called requires a signature from an account owned by the caller program, then the caller program can pass the PDA’s signer seeds as a form of signature. There are many runtime constraints with CPIs, but the most common is the call depth limit of 4. This means that program A → B → C → D is possible, but no further. This has become a limiting factor of building composable programs on Solana.

// Example of a Cross-Program Invocation to the System Program in
// a non-Anchor program
pub fn process_transfer(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let [sender_info, recipient_info, system_program_info] = accounts else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Verify the sender is a signer
    if !sender_info.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Create the System Program Transfer instruction
    let transfer_ix = system_instruction::transfer(
        sender_info.key,
        recipient_info.key,
        amount,
    );

    // Invoke the transfer instruction and pass through the
    // list of accounts required.
    invoke(
        &transfer_ix,
        &[
            sender_info,
            recipient_info,
            system_program_info,
        ],
    )?;
    Ok(())
}

Built‑in programs and sysvars

Solana’s built-in programs, or native programs, are immutable, pre-installed components of the Solana runtime, written in Rust and optimized for core blockchain functionalities. In Ethereum land, these are analogous to the precompiles, but are used a lot less sparingly. When aiming for speed, it makes sense to implement native code for the most frequently called functionalities. These include the System Program for account creation and token transfers, the Stake Program for validator staking, and the BPF Loader for deploying custom programs, among others. Accessible via cross-program invocations, these programs provide essential services that developers can leverage to build applications efficiently. Complementing these, Solana’s system variables (sysvars) are special accounts that store network-wide data, such as the clock (i.e., slot and unix timestamp), recent blockhashes, or rent sysvar for minimum balance calculations. Sysvars are read-only, automatically updated by the runtime (analogous to the block.{timestamp|blockhash| etc.} in Solidity), and accessible to programs for context-aware computations, enabling dynamic interactions with the blockchain’s state without requiring external inputs.

Conclusion

Instruction execution fundamentally differs between EVM and SVM. Each runtime imposes unique constraints on developers. Their state models are so dramatically different that they represent distinct programming paradigms. Consequently, smart contracts with identical functionality require completely different architectural approaches across these two virtual machines.

We hope you enjoyed this post. If we got anything wrong or missed something important, please let us know. Stay tuned for more posts like this one.

Reply

or to participate

Keep Reading

No posts found