├── .gitignore ├── README.md ├── composer.json ├── contracts ├── HelloWorld.sol └── out │ └── HelloWorld.opc └── src ├── EVM.php ├── ExecutionContext.php ├── Memory.php ├── Opcodes ├── Add.php ├── AndOp.php ├── BaseOpcode.php ├── CallDataLoad.php ├── CallDataSize.php ├── CallValue.php ├── Div.php ├── Dup1.php ├── Dup2.php ├── Dup3.php ├── Equal.php ├── GreaterThan.php ├── IsZero.php ├── Jump.php ├── JumpDest.php ├── JumpI.php ├── LessThan.php ├── Mload.php ├── Mod.php ├── Mstore8.php ├── Mul.php ├── OrOp.php ├── Parser.php ├── Pop.php ├── Push1.php ├── Push32.php ├── Push4.php ├── ReturnOp.php ├── Revert.php ├── Shr.php ├── Stop.php ├── Sub.php ├── Swap1.php └── XorOp.php ├── Stack.php ├── TransactionContext.php └── index.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-evm 2 | 3 | This does not work yet. It may never work. Also, I wrote like 96.4% of it at 5AM. So that's what you're getting into. 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "m1guelpf/php-evm", 3 | "type": "library", 4 | "license": "MIT", 5 | "autoload": { 6 | "psr-4": { 7 | "M1guelpf\\EVM\\": "src/" 8 | } 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Miguel Piedrafita", 13 | "email": "soy@miguelpiedrafita.com", 14 | "homepage": "https://miguelpiedrafita.com" 15 | } 16 | ], 17 | "require": { 18 | "larapack/dd": "^1.1", 19 | "illuminate/collections": "^9.0", 20 | "illuminate/support": "^9.0", 21 | "symfony/finder": "^6.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/HelloWorld.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | contract HelloWorld { 5 | fallback(bytes calldata) external returns(bytes memory) { 6 | return bytes("hello world"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /contracts/out/HelloWorld.opc: -------------------------------------------------------------------------------- 1 | 6080604052348015600f57600080fd5b5060408051808201909152600b8082527f68656c6c6f20776f726c64000000000000000000000000000000000000000000602090920191825290f3fea2646970667358221220ee1f006385310a821aa6d86d9f07c883e617c0977bf208a068de1febf72d33f564736f6c634300080b0033 2 | -------------------------------------------------------------------------------- /src/EVM.php: -------------------------------------------------------------------------------- 1 | opcodes = new Parser(); 15 | } 16 | 17 | public function execute(string $code): string 18 | { 19 | $context = new ExecutionContext($code, new TransactionContext()); 20 | 21 | while (! $context->stopped) { 22 | $instruction = $this->opcodes->decode($context); 23 | d("DEBUG: position => {$context->pc}: {$instruction->name}"); 24 | 25 | $instruction->execute($context); 26 | 27 | d($context->stack, $context->memory); 28 | } 29 | 30 | d("Output: 0x{$context->returnData}"); 31 | 32 | return $context->returnData; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ExecutionContext.php: -------------------------------------------------------------------------------- 1 | tx = $tx; 20 | $this->code = array_map('hexdec', str_split($code, 2)); 21 | $this->stack = new Stack(); 22 | $this->memory = new Memory(); 23 | } 24 | 25 | public function getCode(int $steps): int 26 | { 27 | if ($steps != 1) { 28 | throw new RuntimeException("Reading code in more than 1 byte increments not implemented yet."); 29 | } 30 | 31 | return tap( 32 | array_slice($this->code, $this->pc, $steps)[0], 33 | fn () => $this->pc += $steps 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Memory.php: -------------------------------------------------------------------------------- 1 | value[$word] = $value; 12 | } 13 | 14 | public function get(int $word): int 15 | { 16 | if ($word >= count($this->value)) { 17 | return 0; 18 | } 19 | 20 | return $this->value[$word]; 21 | } 22 | 23 | public function multi_get(int $word, int $length): string 24 | { 25 | return rtrim(implode('', array_map(fn (int $word) => dechex($this->get($word)), range($word, $word + $length))), '0'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Opcodes/Add.php: -------------------------------------------------------------------------------- 1 | stack->push( 17 | $context->stack->pop() + $context->stack->pop() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Opcodes/AndOp.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() && $context->stack->pop() ? 1 : 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/BaseOpcode.php: -------------------------------------------------------------------------------- 1 | stack->pop(); 18 | 19 | if ($pos > strlen($context->tx->data)) { 20 | return $context->stack->push(0); 21 | } 22 | 23 | throw new RuntimeException("Loading calldata not yet implemented"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Opcodes/CallDataSize.php: -------------------------------------------------------------------------------- 1 | stack->push(strlen($context->tx->data)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/CallValue.php: -------------------------------------------------------------------------------- 1 | stack->push($context->tx->value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Div.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 17 | 18 | if ($b == 0) { 19 | return $context->stack->push(0); 20 | } 21 | 22 | $context->stack->push($a / $b); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Opcodes/Dup1.php: -------------------------------------------------------------------------------- 1 | stack->pop(); 17 | 18 | $context->stack->push($a); 19 | $context->stack->push($a); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Opcodes/Dup2.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 17 | 18 | $context->stack->push($b); 19 | $context->stack->push($a); 20 | $context->stack->push($b); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Opcodes/Dup3.php: -------------------------------------------------------------------------------- 1 | stack->popN(3); 17 | 18 | $context->stack->push($c); 19 | $context->stack->push($b); 20 | $context->stack->push($a); 21 | $context->stack->push($c); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Opcodes/Equal.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() === $context->stack->pop() ? 1 : 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/GreaterThan.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() > $context->stack->pop() ? 1 : 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/IsZero.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() === 0 ? 1 : 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Jump.php: -------------------------------------------------------------------------------- 1 | stack->pop(); 18 | 19 | if ($dest > count($context->code)) { 20 | throw new RuntimeException("Invalid Jump at {$context->pc}"); 21 | } 22 | 23 | $context->pc = $dest; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Opcodes/JumpDest.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 18 | 19 | if ($cond === 0) { 20 | return; 21 | } 22 | 23 | if ($dest > count($context->code)) { 24 | throw new RuntimeException("Invalid Jump at {$context->pc}"); 25 | } 26 | 27 | $context->pc = $dest; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Opcodes/LessThan.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() < $context->stack->pop() ? 1 : 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Mload.php: -------------------------------------------------------------------------------- 1 | stack->push($context->memory->get($context->stack->pop())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Mod.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 17 | 18 | if ($b == 0) { 19 | return $context->stack->push(0); 20 | } 21 | 22 | $context->stack->push($a % $b); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Opcodes/Mstore8.php: -------------------------------------------------------------------------------- 1 | memory->set($context->stack->pop(), $context->stack->pop()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Mul.php: -------------------------------------------------------------------------------- 1 | stack->push( 17 | $context->stack->pop() * $context->stack->pop() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Opcodes/OrOp.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() || $context->stack->pop() ? 1 : 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Parser.php: -------------------------------------------------------------------------------- 1 | opcodeList = $this->buildOpcodeList(); 21 | } 22 | 23 | public function decode(ExecutionContext $context) 24 | { 25 | if ($context->pc >= count($context->code)) { 26 | return new Stop; 27 | } 28 | 29 | try { 30 | return with($code = $context->getCode(1), fn (int $code) => $this->opcodeList->firstOrFail(fn (OpCode $i) => $i->code === $code)); 31 | } catch (ItemNotFoundException $e) { 32 | throw new RuntimeException(sprintf("Opcode 0x%s not found, see https://www.evm.codes/#%s", dechex($code), dechex($code)), 0, $e); 33 | } 34 | } 35 | 36 | protected function buildOpcodeList(): Collection 37 | { 38 | $opcodeList = collect(); 39 | 40 | foreach ((new Finder)->in([__DIR__])->files() as $instruction) { 41 | $instruction = '\M1guelpf\EVM\Opcodes\\'.str_replace('.php', '', Str::after($instruction->getRealPath(), __DIR__.DIRECTORY_SEPARATOR)); 42 | 43 | if (is_subclass_of($instruction, BaseOpcode::class) && ! (new ReflectionClass($instruction))->isAbstract()) { 44 | $opcodeList->push(new $instruction); 45 | } 46 | } 47 | 48 | return $opcodeList; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Opcodes/Pop.php: -------------------------------------------------------------------------------- 1 | stack->pop(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Push1.php: -------------------------------------------------------------------------------- 1 | stack->push($context->getCode(1)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Push32.php: -------------------------------------------------------------------------------- 1 | execute($context); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Push4.php: -------------------------------------------------------------------------------- 1 | execute($context); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/ReturnOp.php: -------------------------------------------------------------------------------- 1 | stopped = true; 17 | $context->returnData = $context->memory->multi_get($context->stack->pop(), $context->stack->pop()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Opcodes/Revert.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 18 | $context->returnData = $context->memory->multi_get($word, $length); 19 | $context->stopped = true; 20 | 21 | throw new RuntimeException("Reverted."); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Opcodes/Shr.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 17 | 18 | if ($shift > 255) { 19 | return $context->stack->push(0); 20 | } 21 | 22 | $context->stack->push( 23 | $value >> $shift 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Opcodes/Stop.php: -------------------------------------------------------------------------------- 1 | stopped = true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Opcodes/Sub.php: -------------------------------------------------------------------------------- 1 | stack->push( 17 | $context->stack->pop() - $context->stack->pop() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Opcodes/Swap1.php: -------------------------------------------------------------------------------- 1 | stack->popN(2); 17 | 18 | $context->stack->push($a); 19 | $context->stack->push($b); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Opcodes/XorOp.php: -------------------------------------------------------------------------------- 1 | stack->push($context->stack->pop() ^ $context->stack->pop()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Stack.php: -------------------------------------------------------------------------------- 1 | 2 ** 256 - 1) { 15 | throw new RuntimeException("Invalid stack push"); 16 | } 17 | 18 | array_push($this->value, $item); 19 | } 20 | 21 | public function pop() : int 22 | { 23 | return array_pop($this->value); 24 | } 25 | 26 | public function popN(int $lenght) : array 27 | { 28 | return Collection::times($lenght, fn () => array_pop($this->value))->toArray(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/TransactionContext.php: -------------------------------------------------------------------------------- 1 | execute($hello_world); 12 | --------------------------------------------------------------------------------