Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/fjl/geas
The Good Ethereum Assembler
https://github.com/fjl/geas
ethereum evm
Last synced: 5 days ago
JSON representation
The Good Ethereum Assembler
- Host: GitHub
- URL: https://github.com/fjl/geas
- Owner: fjl
- License: lgpl-3.0
- Created: 2023-09-11T19:51:00.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2024-11-27T01:13:26.000Z (about 2 months ago)
- Last Synced: 2025-01-07T19:11:52.405Z (13 days ago)
- Topics: ethereum, evm
- Language: Go
- Homepage:
- Size: 255 KB
- Stars: 95
- Watchers: 3
- Forks: 6
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
![geas](assets/geas-b.svg)
This is geas – the Good Ethereum Assembler[^1] – a macro assembler for the EVM.
You can use it to create any contract for Ethereum, though it's probably a bad idea.
For real contracts, you should use a well-tested language compiler like Solidity.
The purpose of geas is mostly creating specialty programs and tinkering with the EVM
at a low level.### Installation
You can use the `go` tool to install the latest released version.
This creates a `geas` binary in the current directory:env "GOBIN=$PWD" go install github.com/fjl/geas@latest
For development of geas, clone the repository and then run `go build ./cmd/geas`.
### Usage
To create bytecode from an assembly file, run the tool with a filename as argument.
./geas file.eas
There is also a disassembler. To disassemble hex bytecode from standard input, run:
./geas -d -
To see all supported flags, run `geas` with no arguments.
### Editor Support
VIM users may be interested in [vim-geas](https://github.com/lightclient/vim-geas).
### Use as a Go Library
You can also use the assembler as a library. See the [API documentation](https://pkg.go.dev/github.com/fjl/geas/asm)
to get started.## Language
Programs accepted by the assembler follow a simple structure. Each line is an instruction.
Both uppercase and lowercase can be used for instruction names. All known EVM instructions
are supported.Comments can appear anywhere and are introduced by the semicolon (;) character.
push 1 ;; comment
push 2
addOpcodes listed in the program correspond directly with the bytecodes in output.
### Jump
Jump destinations are written as a label followed by colon (:) and can be referred to
using the notation `@label` together with JUMP or JUMPI.begin:
push 1
push 2
add
jump @beginWhen using JUMP with an argument, it turns into a PUSH of the label followed by the jump
instruction, so the above is equivalent to:begin:
push 1
push 2
add
push @begin
jumpIt is also possible to create labels without emitting a JUMPDEST instruction by prefixing
the label name with the dot (.) character. While dotted labels are not valid for use as an
argument to JUMP, they can be used with PUSH to measure code offsets.push @.end
codesize
eq
.end:### Push
The EVM instruction has sized push instructions from size zero (`PUSH0`) up to a size of
32 bytes (`PUSH32`). While you can use sized push instructions directly, it is preferable
to let the assembler figure out the right size for you. To do this use the variable-size
`PUSH` instruction.All PUSH-type instructions must be followed by an immediate argument on the same line.
Simple math expressions and label references can be used within the argument:.begin:
push (@add_it * 2) - 3
push 5
add_it:
addSupported arithmetic operations include addition (+), subtraction (-), multiplication (*),
division (/), and modulo (%). There is also support for bit-shifts (<<, >>), bitwise AND
(&), OR (|), XOR (^).All arithmetic is performed with arbitrary precision integers. The result of calculations
must fit into 256 bits in order to be valid as a PUSH argument. For sized push, the result
must fit into the declared push size. Negative results are not allowed.### Expression Macros
Expression macros can be created with the `#define` directive. Macros can be used within
PUSH argument expressions.Macros can have parameters. Refer to parameter values using the dollar sign ($) prefix
within the macro.#define z = 0x8823
#define myexpr(x, y) = ($x + $y) * zpush myexpr(1, 2)
### Builtin Macros
There are several builtin macros for common EVM tasks. Names of builtins start with a dot,
and builtin macros cannot be redefined. Available builtins include:`.abs()` for getting the absolute value of a number:
push .abs(0 - 100)
`.selector()` for computing 4-byte ABI selectors:
push .selector("transfer(address,uint256)")
push 0
mstore`.keccak256()`, `.sha256()` hash functions:
push .sha256("data")
`.address()` for declaring contract addresses. The checksum and byte length of the address
are verified.#define otherContract = .address(0x658bdf435d810c91414ec09147daa6db62406379)
### Instruction Macros
Common groups of instructions can be defined as instruction macros. Names of such macros
always start with the percent (%) character.#define %add5_and_store(x, location) {
push $x
push 5
add
push $location
mstore
}To invoke an instruction macro, write the macro name as a statement on its own line. If
the macro has no arguments, you can also leave the parentheses off..begin:
%add5_and_store(3, 64)
%add5_and_store(4, 32)
push 32
push 64
sha3Nested macro definitions are not allowed. Macro recursion is also not allowed.
When defining (local) labels within instruction macros, they will only be visible within
the macro. There is no way to refer to a local macro label from the outside, though you
can pass references to such internal labels into another macro. The example below
illustrates this, and also shows that in order to jump to a label argument within a macro,
you must use explicit PUSH and JUMP.#define %jump_if_not(label) {
iszero
push $label
jumpi
}#define %read_input(bytes) {
calldatasize
push $bytes
eq
%jump_if_not(@revert)push 0
push $bytes
calldataload
jump @continuerevert:
push 0
push 0
revertcontinue:
}### Including Files
EVM assembly files can be included into the current program using the `#include`
directive. Top-level instructions in the included file will be inserted at the position of
the directive.`#include` filenames are resolved relative to the file containing the directive.
.begin:
push @.end
push 32
mstore#include "file.evm"
.end:### Local and Global Scope
Names of labels and macros are case-sensitive. And just like in Go, the case of the first
letter determines visibility of definitions.Macro and label definitions whose name begins with a lower-case letter are local to the
file they're defined in. This means local definitions cannot be referenced by `#include`
files.Identifiers beginning with an upper-case letter are registered in the global scope and are
available for use across files. When using `#include`, global definitions in the included
file also become available in all other files.Global identifiers must be unique across the program, i.e. they can only be defined once.
Files defining global macros or labels can only be included into the program once. Note
that the uniqueness requirement also means that instruction macros containing global
labels can only be called once. Use good judgement when structuring your includes to avoid
redefinition errors.lib.eas:
#define result = 128
#define StoreSum {
add
push result
mstore
}main.eas:
#include "lib.eas"
push 1
push 2
%StoreSum ;; calling global macro defined in lib.evm### Configuring the target instruction set
The EVM is a changing environment. Opcodes may be added (and sometimes removed) as new
versions of the EVM are released in protocol forks. Geas is aware of EVM forks and their
respective instruction sets.Geas always operates on a specific EVM instruction set. It targets the latest known eth
mainnet fork by default, i.e. all opcodes available in that fork can be used, and opcodes
that have been removed in any prior fork cannot.Use the `#pragma target` directive to change the target instruction set. The basic syntax is
#pragma target "name"
where `name` is a lower-case execution-layer fork name like `homestead`, `berlin`, or `prague`.
Here is an example. This contract uses the CHAINID instruction to check if it is running
on mainnet, and destroys itself otherwise. CHAINID became available in the "istanbul"
fork, and SELFDESTRUCT was removed in a later revision of the EVM, so this program is only
applicable to a certain range of past EVM versions.#pragma target "berlin"
chainid ; [id]
push 1 ; [1, id]
eq ; [id = 1]
jumpi @mainnet ; []
push 0x0 ; [zeroaddr]
selfdestruct ; []
mainnet:Note that declaring the target instruction set using `#pragma target` will not prevent the
output bytecode from running on a different EVM version, since it is just a compiler
setting. The example program above will start behaving differently from its intended
version on EVM version "cancun", because SELFDESTRUCT was turned into SENDALL in that
fork. It may even stop working entirely in a later fork.`#pragma target` can only appear in the program once. It cannot be placed in an include
file. You have to put the directive in the main program file.### #assemble
When writing contract constructors and advanced CALL scenarios, it can be necessary to
include subprogram bytecode as-is. The `#assemble` directive does this for you.Using `#assemble` runs the assembler on the specified file, and includes the resulting
bytecode into the current program. Labels of the subprogram will start at offset zero.
Unlike with `#include`, global definitions of the subprogram are not imported.;; copy subprogram to memory
push @.end - @.begin ; [size]
push @.begin ; [offset, size]
push 128 ; [dest, offset, codesize]
codecopy ; [].begin:
#assemble "subprogram.eas"
.endIf a target instruction set is configured with `#pragma target`, it will also be used for
assembling the subprogram. However, the subprogram file can override the instruction set
using its own `#pragma target` directive.[^1]: Under no circumstances must it be called the geth assembler.