What Is the EVM?

The EVM is the part of Ethereum that handles smart contract deployment and execution. Simple value transfer transactions from one EOA to another don’t need to involve it, practically speaking, but everything else will involve a state update computed by the EVM. At a high level, the EVM running on the Ethereum blockchain can be thought of as a global decentralized computer containing millions of executable objects, each with its own permanent data store.

The EVM is a quasi–Turing-complete state machine; “quasi” because all execution processes are limited to a finite number of computational steps by the amount of gas available for any given smart contract execution. As such, the halting problem is “solved” (all program executions will halt) and the situation where execution might (accidentally or maliciously) run forever, thus bringing the Ethereum platform to halt in its entirety, is avoided.

The EVM has a stack-based architecture, storing all in-memory values on a stack. It works with a word size of 256 bits (mainly to facilitate native hashing and elliptic curve operations) and has several addressable data components:

  • An immutable program code ROM, loaded with the bytecode of the smart contract to be executed

  • A volatile memory, with every location explicitly initialized to zero

  • A permanent storage that is part of the Ethereum state, also zero-initialized

There is also a set of environment variables and data that is available during execution. We will go through these in more detail later in this chapter.

The Ethereum Virtual Machine (EVM) Architecture and Execution Context shows the EVM architecture and execution context.

The Ethereum Virtual Machine (EVM) Architecture and Execution Context

Figure 1. The Ethereum Virtual Machine (EVM) Architecture and Execution Context

Comparison with Existing Technology

The term “virtual machine” is often applied to the virtualization of a real computer, typically by a “hypervisor” such as VirtualBox or QEMU, or of an entire operating system instance, such as Linux’s KVM. These must provide a software abstraction, respectively, of actual hardware, and of system calls and other kernel functionality.

The EVM operates in a much more limited domain: it is just a computation engine, and as such provides an abstraction of just computation and storage, similar to the Java Virtual Machine (JVM) specification, for example. From a high-level viewpoint, the JVM is designed to provide a runtime environment that is agnostic of the underlying host OS or hardware, enabling compatibility across a wide variety of systems. High-level programming languages such as Java or Scala (which use the JVM) or C# (which uses .NET) are compiled into the bytecode instruction set of their respective virtual machine. In the same way, the EVM executes its own bytecode instruction set (described in the next section), which higher-level smart contract programming languages such as LLL, Serpent, Mutan, or Solidity are compiled into.

The EVM, therefore, has no scheduling capability, because execution ordering is organized externally to it—Ethereum clients run through verified block transactions to determine which smart contracts need executing and in which order. In this sense, the Ethereum world computer is single-threaded, like JavaScript. Neither does the EVM have any “system interface” handling or “hardware support”—there is no physical machine to interface with. The Ethereum world computer is completely virtual.

The EVM Instruction Set (Bytecode Operations)

The EVM instruction set offers most of the operations you might expect, including:

  • Arithmetic and bitwise logic operations

  • Execution context inquiries

  • Stack, memory, and storage access

  • Control flow operations

  • Logging, calling, and other operators

In addition to the typical bytecode operations, the EVM also has access to account information (e.g., address and balance) and block information (e.g., block number and current gas price).

Let’s start our exploration of the EVM in more detail by looking at the available opcodes and what they do. As you might expect, all operands are taken from the stack, and the result (where applicable) is often put back on the top of the stack.

Note

A complete list of opcodes and their corresponding gas cost can be found in [evm_opcodes].

The available opcodes can be divided into the following categories:

Arithmetic operations

Arithmetic opcode instructions:

  1. ADD //Add the top two stack items
  2. MUL //Multiply the top two stack items
  3. SUB //Subtract the top two stack items
  4. DIV //Integer division
  5. SDIV //Signed integer division
  6. MOD //Modulo (remainder) operation
  7. SMOD //Signed modulo operation
  8. ADDMOD //Addition modulo any number
  9. MULMOD //Multiplication modulo any number
  10. EXP //Exponential operation
  11. SIGNEXTEND //Extend the length of a two's complement signed integer
  12. SHA3 //Compute the Keccak-256 hash of a block of memory

Note that all arithmetic is performed modulo 2256 (unless otherwise noted), and that the zeroth power of zero, 00, is taken to be 1.

Stack operations

Stack, memory, and storage management instructions:

  1. POP //Remove the top item from the stack
  2. MLOAD //Load a word from memory
  3. MSTORE //Save a word to memory
  4. MSTORE8 //Save a byte to memory
  5. SLOAD //Load a word from storage
  6. SSTORE //Save a word to storage
  7. MSIZE //Get the size of the active memory in bytes
  8. PUSHx //Place x byte item on the stack, where x can be any integer from
  9. // 1 to 32 (full word) inclusive
  10. DUPx //Duplicate the x-th stack item, where x can be any integer from
  11. // 1 to 16 inclusive
  12. SWAPx //Exchange 1st and (x+1)-th stack items, where x can be any
  13. // integer from 1 to 16 inclusive

Process flow operations

Instructions for control flow:

  1. STOP //Halt execution
  2. JUMP //Set the program counter to any value
  3. JUMPI //Conditionally alter the program counter
  4. PC //Get the value of the program counter (prior to the increment
  5. //corresponding to this instruction)
  6. JUMPDEST //Mark a valid destination for jumps

System operations

Opcodes for the system executing the program:

  1. LOGx //Append a log record with x topics, where x is any integer
  2. //from 0 to 4 inclusive
  3. CREATE //Create a new account with associated code
  4. CALL //Message-call into another account, i.e. run another
  5. //account's code
  6. CALLCODE //Message-call into this account with another
  7. //account's code
  8. RETURN //Halt execution and return output data
  9. DELEGATECALL //Message-call into this account with an alternative
  10. //account's code, but persisting the current values for
  11. //sender and value
  12. STATICCALL //Static message-call into an account
  13. REVERT //Halt execution, reverting state changes but returning
  14. //data and remaining gas
  15. INVALID //The designated invalid instruction
  16. SELFDESTRUCT //Halt execution and register account for deletion

Logic operations

Opcodes for comparisons and bitwise logic:

  1. LT //Less-than comparison
  2. GT //Greater-than comparison
  3. SLT //Signed less-than comparison
  4. SGT //Signed greater-than comparison
  5. EQ //Equality comparison
  6. ISZERO //Simple NOT operator
  7. AND //Bitwise AND operation
  8. OR //Bitwise OR operation
  9. XOR //Bitwise XOR operation
  10. NOT //Bitwise NOT operation
  11. BYTE //Retrieve a single byte from a full-width 256-bit word

Environmental operations

Opcodes dealing with execution environment information:

  1. GAS //Get the amount of available gas (after the reduction for
  2. //this instruction)
  3. ADDRESS //Get the address of the currently executing account
  4. BALANCE //Get the account balance of any given account
  5. ORIGIN //Get the address of the EOA that initiated this EVM
  6. //execution
  7. CALLER //Get the address of the caller immediately responsible
  8. //for this execution
  9. CALLVALUE //Get the ether amount deposited by the caller responsible
  10. //for this execution
  11. CALLDATALOAD //Get the input data sent by the caller responsible for
  12. //this execution
  13. CALLDATASIZE //Get the size of the input data
  14. CALLDATACOPY //Copy the input data to memory
  15. CODESIZE //Get the size of code running in the current environment
  16. CODECOPY //Copy the code running in the current environment to
  17. //memory
  18. GASPRICE //Get the gas price specified by the originating
  19. //transaction
  20. EXTCODESIZE //Get the size of any account's code
  21. EXTCODECOPY //Copy any account's code to memory
  22. RETURNDATASIZE //Get the size of the output data from the previous call
  23. //in the current environment
  24. RETURNDATACOPY //Copy data output from the previous call to memory

Block operations

Opcodes for accessing information on the current block:

  1. BLOCKHASH //Get the hash of one of the 256 most recently completed
  2. //blocks
  3. COINBASE //Get the block's beneficiary address for the block reward
  4. TIMESTAMP //Get the block's timestamp
  5. NUMBER //Get the block's number
  6. DIFFICULTY //Get the block's difficulty
  7. GASLIMIT //Get the block's gas limit

Ethereum State

The job of the EVM is to update the Ethereum state by computing valid state transitions as a result of smart contract code execution, as defined by the Ethereum protocol. This aspect leads to the description of Ethereum as a transaction-based state machine, which reflects the fact that external actors (i.e., account holders and miners) initiate state transitions by creating, accepting, and ordering transactions. It is useful at this point to consider what constitutes the Ethereum state.

At the top level, we have the Ethereum world state. The world state is a mapping of Ethereum addresses (160-bit values) to accounts. At the lower level, each Ethereum address represents an account comprising an ether balance (stored as the number of wei owned by the account), a nonce (representing the number of transactions successfully sent from this account if it is an EOA, or the number of contracts created by it if it is a contract account), the account’s storage (which is a permanent data store, only used by smart contracts), and the account’s program code (again, only if the account is a smart contract account). An EOA will always have no code and an empty storage.

When a transaction results in smart contract code execution, an EVM is instantiated with all the information required in relation to the current block being created and the specific transaction being processed. In particular, the EVM’s program code ROM is loaded with the code of the contract account being called, the program counter is set to zero, the storage is loaded from the contract account’s storage, the memory is set to all zeros, and all the block and environment variables are set. A key variable is the gas supply for this execution, which is set to the amount of gas paid for by the sender at the start of the transaction (see Gas for more details). As code execution progresses, the gas supply is reduced according to the gas cost of the operations executed. If at any point the gas supply is reduced to zero we get an “Out of Gas” (OOG) exception; execution immediately halts and the transaction is abandoned. No changes to the Ethereum state are applied, except for the sender’s nonce being incremented and their ether balance going down to pay the block’s beneficiary for the resources used to execute the code to the halting point. At this point, you can think of the EVM running on a sandboxed copy of the Ethereum world state, with this sandboxed version being discarded completely if execution cannot complete for whatever reason. However, if execution does complete successfully, then the real-world state is updated to match the sandboxed version, including any changes to the called contract’s storage data, any new contracts created, and any ether balance transfers that were initiated.

Note that because a smart contract can itself effectively initiate transactions, code execution is a recursive process. A contract can call other contracts, with each call resulting in another EVM being instantiated around the new target of the call. Each instantiation has its sandbox world state initialized from the sandbox of the EVM at the level above. Each instantiation is also given a specified amount of gas for its gas supply (not exceeding the amount of gas remaining in the level above, of course), and so may itself halt with an exception due to being given too little gas to complete its execution. Again, in such cases, the sandbox state is discarded, and execution returns to the EVM at the level above.

Compiling Solidity to EVM Bytecode

Compiling a Solidity source file to EVM bytecode can be accomplished via several methods. In [intro_chapter] we used the online Remix compiler. In this chapter, we will use the solc executable at the command line. For a list of options, run the following command:

  1. $ solc --help

Generating the raw opcode stream of a Solidity source file is easily achieved with the —opcodes command-line option. This opcode stream leaves out some information (the —asm option produces the full information), but it is sufficient for this discussion. For example, compiling an example Solidity file, Example.sol, and sending the opcode output into a directory named BytecodeDir is accomplished with the following command:

  1. $ solc -o BytecodeDir --opcodes Example.sol

or:

  1. $ solc -o BytecodeDir --asm Example.sol

The following command will produce the bytecode binary for our example program:

  1. $ solc -o BytecodeDir --bin Example.sol

The output opcode files generated will depend on the specific contracts contained within the Solidity source file. Our simple Solidity file Example.sol has only one contract, named example:

  1. pragma solidity ^0.4.19;
  2. contract example {
  3. address contractOwner;
  4. function example() {
  5. contractOwner = msg.sender;
  6. }
  7. }

As you can see, all this contract does is hold one persistent state variable, which is set as the address of the last account to run this contract.

If you look in the BytecodeDir directory you will see the opcode file example.opcode, which contains the EVM opcode instructions of the example contract. Opening the example.opcode file in a text editor will show the following:

  1. PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1
  2. REVERT JUMPDEST CALLER PUSH1 0x0 DUP1 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH20
  3. 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MUL NOT AND SWAP1 DUP4 PUSH20
  4. 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND MUL OR SWAP1 SSTORE POP PUSH1
  5. 0x35 DUP1 PUSH1 0x5B PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1
  6. 0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 JUMP
  7. 0xb9 SWAP14 0xcb 0x1e 0xdd RETURNDATACOPY 0xec 0xe0 0x1f 0x27 0xc9 PUSH5
  8. 0x9C5ABCC14A NUMBER 0x5e INVALID EXTCODESIZE 0xdb 0xcf EXTCODESIZE 0x27
  9. EXTCODESIZE 0xe2 0xb8 SWAP10 0xed 0x

Compiling the example with the —asm option produces a file named example.evm in our BytecodeDir directory. This contains a slightly higher-level description of the EVM bytecode instructions, together with some helpful annotations:

  1. /* "Example.sol":26:132 contract example {... */
  2. mstore(0x40, 0x60)
  3. /* "Example.sol":74:130 function example() {... */
  4. jumpi(tag_1, iszero(callvalue))
  5. 0x0
  6. dup1
  7. revert
  8. tag_1:
  9. /* "Example.sol":115:125 msg.sender */
  10. caller
  11. /* "Example.sol":99:112 contractOwner */
  12. 0x0
  13. dup1
  14. /* "Example.sol":99:125 contractOwner = msg.sender */
  15. 0x100
  16. exp
  17. dup2
  18. sload
  19. dup2
  20. 0xffffffffffffffffffffffffffffffffffffffff
  21. mul
  22. not
  23. and
  24. swap1
  25. dup4
  26. 0xffffffffffffffffffffffffffffffffffffffff
  27. and
  28. mul
  29. or
  30. swap1
  31. sstore
  32. pop
  33. /* "Example.sol":26:132 contract example {... */
  34. dataSize(sub_0)
  35. dup1
  36. dataOffset(sub_0)
  37. 0x0
  38. codecopy
  39. 0x0
  40. return
  41. stop
  42. sub_0: assembly {
  43. /* "Example.sol":26:132 contract example {... */
  44. mstore(0x40, 0x60)
  45. 0x0
  46. dup1
  47. revert
  48. auxdata: 0xa165627a7a7230582056b99dcb1edd3eece01f27c9649c5abcc14a435efe3b...
  49. }

The —bin-runtime option produces the machine-readable hexadecimal bytecode:

  1. 60606040523415600e57600080fd5b336000806101000a81548173
  2. ffffffffffffffffffffffffffffffffffffffff
  3. 021916908373
  4. ffffffffffffffffffffffffffffffffffffffff
  5. 160217905550603580605b6000396000f3006060604052600080fd00a165627a7a7230582056b...

You can investigate what’s going on here in detail using the opcode list given in The EVM Instruction Set (Bytecode Operations). However, that’s quite a task, so let’s just start by examining the first four instructions:

  1. PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE

Here we have PUSH1 followed by a raw byte of value 0x60. This EVM instruction takes the single byte following the opcode in the program code (as a literal value) and pushes it onto the stack. It is possible to push values of size up to 32 bytes onto the stack, as in:

  1. PUSH32 0x436f6e67726174756c6174696f6e732120536f6f6e20746f206d617374657221

The second PUSH1 opcode from example.opcode stores 0x40 onto the top of the stack (pushing the 0x60 already present there down one slot).

Next is MSTORE, which is a memory store operation that saves a value to the EVM’s memory. It takes two arguments and, like most EVM operations, obtains them from the stack. For each argument the stack is “popped”; i.e., the top value on the stack is taken off and all the other values on the stack are shifted up one position. The first argument for MSTORE is the address of the word in memory where the value to be saved will be put. For this program we have 0x40 at the top of the stack, so that is removed from the stack and used as the memory address. The second argument is the value to be saved, which is 0x60 here. After the MSTORE operation is executed our stack is empty again, but we have the value 0x60 (96 in decimal) at the memory location 0x40.

The next opcode is CALLVALUE, which is an environmental opcode that pushes onto the top of the stack the amount of ether (measured in wei) sent with the message call that initiated this execution.

We could continue to step through this program in this way until we had a full understanding of the low-level state changes that this code effects, but it wouldn’t help us at this stage. We’ll come back to it later in the chapter.

Contract Deployment Code

There is an important but subtle difference between the code used when creating and deploying a new contract on the Ethereum platform and the code of the contract itself. In order to create a new contract, a special transaction is needed that has its to field set to the special 0x0 address and its data field set to the contract’s initiation code. When such a contract creation transaction is processed, the code for the new contract account is not the code in the data field of the transaction. Instead, an EVM is instantiated with the code in the data field of the transaction loaded into its program code ROM, and then the output of the execution of that deployment code is taken as the code for the new contract account. This is so that new contracts can be programmatically initialized using the Ethereum world state at the time of deployment, setting values in the contract’s storage and even sending ether or creating further new contracts.

When compiling a contract offline, e.g., using solc on the command line, you can either get the deployment bytecode or the runtime bytecode.

The deployment bytecode is used for every aspect of the initialization of a new contract account, including the bytecode that will actually end up being executed when transactions call this new contract (i.e., the runtime bytecode) and the code to initialize everything based on the contract’s constructor.

The runtime bytecode, on the other hand, is exactly the bytecode that ends up being executed when the new contract is called, and nothing more; it does not include the bytecode needed to initialize the contract during deployment.

Let’s take the simple Faucet.sol contract we created earlier as an example:

  1. // Version of Solidity compiler this program was written for
  2. pragma solidity ^0.4.19;
  3. // Our first contract is a faucet!
  4. contract Faucet {
  5. // Give out ether to anyone who asks
  6. function withdraw(uint withdraw_amount) public {
  7. // Limit withdrawal amount
  8. require(withdraw_amount <= 100000000000000000);
  9. // Send the amount to the address that requested it
  10. msg.sender.transfer(withdraw_amount);
  11. }
  12. // Accept any incoming amount
  13. function () external payable {}
  14. }

To get the deployment bytecode, we would run solc --bin Faucet.sol. If we instead wanted just the runtime bytecode, we would run solc --bin-runtime Faucet.sol.

If you compare the output of these commands, you will see that the runtime bytecode is a subset of the deployment bytecode. In other words, the runtime bytecode is entirely contained within the deployment bytecode.

Disassembling the Bytecode

Disassembling EVM bytecode is a great way to understand how high-level Solidity acts in the EVM. There are a few disassemblers you can use to do this:

  • Porosity is a popular open source decompiler.

  • Ethersplay is an EVM plug-in for Binary Ninja, a disassembler.

  • IDA-Evm is an EVM plugin for IDA, another disassembler.

In this section, we will be using the Ethersplay plug-in for Binary Ninja and to start Disassembling the Faucet runtime bytecode. After getting the runtime bytecode of Faucet.sol, we can feed it into Binary Ninja (after loading the Ethersplay plug-in) to see what the EVM instructions look like.

Faucet.sol runtime bytecode disassembled

Figure 2. Disassembling the Faucet runtime bytecode

When you send a transaction to an ABI-compatible smart contract (which you can assume all contracts are), the transaction first interacts with that smart contract’s dispatcher. The dispatcher reads in the data field of the transaction and sends the relevant part to the appropriate function. We can see an example of a dispatcher at the beginning of our disassembled Faucet.sol runtime bytecode. After the familiar MSTORE instruction, we see the following instructions:

  1. PUSH1 0x4
  2. CALLDATASIZE
  3. LT
  4. PUSH1 0x3f
  5. JUMPI

As we have seen, PUSH1 0x4 places 0x4 onto the top of the stack, which is otherwise empty. CALLDATASIZE gets the size in bytes of the data sent with the transaction (known as the calldata) and pushes that number onto the stack. After these operations have been executed, the stack looks like this:

Stack

<length of calldata from tx>

0x4

This next instruction is LT, short for “less than.” The LT instruction checks whether the top item on the stack is less than the next item on the stack. In our case, it checks to see if the result of CALLDATASIZE is less than 4 bytes.

Why does the EVM check to see that the calldata of the transaction is at least 4 bytes? Because of how function identifiers work. Each function is identified by the first 4 bytes of its Keccak-256 hash. By placing the function’s name and what arguments it takes into a keccak256 hash function, we can deduce its function identifier. In our case, we have:

  1. keccak256("withdraw(uint256)") = 0x2e1a7d4d...

Thus, the function identifier for the withdraw(uint256) function is 0x2e1a7d4d, since these are the first 4 bytes of the resulting hash. A function identifier is always 4 bytes long, so if the entire data field of the transaction sent to the contract is less than 4 bytes, then there’s no function with which the transaction could possibly be communicating, unless a fallback function is defined. Because we implemented such a fallback function in Faucet.sol, the EVM jumps to this function when the calldata’s length is less than 4 bytes.

LT pops the top two values off the stack and, if the transaction’s data field is less than 4 bytes, pushes 1 onto it. Otherwise, it pushes 0. In our example, let’s assume the data field of the transaction sent to our contract was less than 4 bytes.

The PUSH1 0x3f instruction pushes the byte 0x3f onto the stack. After this instruction, the stack looks like this:

Stack

0x3f

1

The next instruction is JUMPI, which stands for “jump if.” It works like so:

  1. jumpi(label, cond) // Jump to "label" if "cond" is true

In our case, label is 0x3f, which is where our fallback function lives in our smart contract. The cond argument is 1, which was the result of the LT instruction earlier. To put this entire sequence into words, the contract jumps to the fallback function if the transaction data is less than 4 bytes.

At 0x3f, only a STOP instruction follows, because although we declared a fallback function, we kept it empty. As you can see in JUMPI instruction leading to fallback function, had we not implemented a fallback function, the contract would throw an exception instead.

JUMPI instruction leading to fallback function

Figure 3. JUMPI instruction leading to fallback function

Let’s examine the central block of the dispatcher. Assuming we received calldata that was greater than 4 bytes in length, the JUMPI instruction would not jump to the fallback function. Instead, code execution would proceed to the following instructions:

  1. PUSH1 0x0
  2. CALLDATALOAD
  3. PUSH29 0x1000000...
  4. SWAP1
  5. DIV
  6. PUSH4 0xffffffff
  7. AND
  8. DUP1
  9. PUSH4 0x2e1a7d4d
  10. EQ
  11. PUSH1 0x41
  12. JUMPI

PUSH1 0x0 pushes 0 onto the stack, which is now otherwise empty again. CALLDATALOAD accepts as an argument an index within the calldata sent to the smart contract and reads 32 bytes from that index, like so:

  1. calldataload(p) //load 32 bytes of calldata starting from byte position p

Since 0 was the index passed to it from the PUSH1 0x0 command, CALLDATALOAD reads 32 bytes of calldata starting at byte 0, and then pushes it to the top of the stack (after popping the original 0x0). After the PUSH29 0x1000000…​ instruction, the stack is then:

Stack

0x1000000…​ (29 bytes in length)

<32 bytes of calldata starting at byte 0>

SWAP1 switches the top element on the stack with the i-th element after it. In this case, it swaps 0x1000000…​ with the calldata. The new stack is:

Stack

<32 bytes of calldata starting at byte 0>

0x1000000…​ (29 bytes in length)

The next instruction is DIV, which works as follows:

  1. div(x, y) // integer division x / y

In this case, x = 32 bytes of calldata starting at byte 0, and y = 0x100000000…​ (29 bytes total). Can you think of why the dispatcher is doing the division? Here’s a hint: we read 32 bytes from calldata earlier, starting at index 0. The first 4 bytes of that calldata is the function identifier.

The 0x100000000…​ we pushed earlier is 29 bytes long, consisting of a 1 at the beginning, followed by all 0s. Dividing our 32 bytes of calldata by this value will leave us only the topmost 4 bytes of our calldata load, starting at index 0. These 4 bytes—the first 4 bytes in the calldata starting at index 0—are the function identifier, and this is how the EVM extracts that field.

If this part isn’t clear to you, think of it like this: in base 10, 1234000 / 1000 = 1234. In base 16, this is no different. Instead of every place being a multiple of 10, it is a multiple of 16. Just as dividing by 103 (1000) in our smaller example kept only the topmost digits, dividing our 32-byte base 16 value by 1629 does the same.

The result of the DIV (the function identifier) gets pushed onto the stack, and our stack is now:

Stack

<function identifier sent in data>

Since the PUSH4 0xffffffff and AND instructions are redundant, we can ignore them entirely, as the stack will remain the same after they are done. The DUP1 instruction duplicates the first item on the stack, which is the function identifier. The next instruction, PUSH4 0x2e1a7d4d, pushes the precalculated function identifier of the withdraw(uint256) function onto the stack. The stack is now:

Stack

0x2e1a7d4d

<function identifier sent in data>

<function identifier sent in data>

The next instruction, EQ, pops off the top two items of the stack and compares them. This is where the dispatcher does its main job: it compares whether the function identifier sent in the msg.data field of the transaction matches that of withdraw(uint256). If they are equal, EQ pushes 1 onto the stack, which will ultimately be used to jump to the withdraw function. Otherwise, EQ pushes 0 onto the stack.

Assuming the transaction sent to our contract indeed began with the function identifier for withdraw(uint256), our stack has become:

Stack

1

<function identifier sent in data> (now known to be 0x2e1a7d4d)

Next, we have PUSH1 0x41, which is the address at which the withdraw(uint256) function lives in the contract. After this instruction, the stack looks like this:

Stack

0x41

1

function identifier sent in msg.data

The JUMPI instruction is next, and it once again accepts the top two elements on the stack as arguments. In this case, we have jumpi(0x41, 1), which tells the EVM to execute the jump to the location of the withdraw(uint256) function, and the execution of that function’s code can proceed.