Penetration Test Report (Consensys): vyper audit 2019 10

Vyper Security Review
1 Scope Date October 2019
2 Executive Summary
Reviewers Steve Marx, Todd Proebsting
3 Compiler Decomposition
4 Abstract Syntax Trees (AST)
5 Separate Type Checking Pass
6 Intermediate Representation (LLL)
7 Needed Specications
7.1 Language Specication
7.2 Interface Specications
8 Bytecode Verication
8.1 “Valency” in the code generator
9 Auditable Code
9.1 Refactor Long Functions
9.2 Use Static Typing
9.3 Avoid Code Repetition
10 Issues
Appendix 1 - Disclosure
1 Scope
ConsenSys Diligence conducted a preliminary review of beta 13 of the Vyper compiler. The
goal of this review was to identify what work remains before the compiler is ready for a full
security audit.
2 Executive Summary
Auditing code requires comparing the code to expected behavior. For a compiler, the
expected behavior is determined by the source language specication and the target
machine. To condently audit a Vyper compiler, a complete and precise Vyper specication
is needed.
The Vyper compiler is currently decomposed into three passes: a small pass that
translates Python abstract syntax trees to Vyper ASTs, a large pass that translates Vyper
ASTs to LLL intermediate representation, and a small pass that translates LLL to EVM
bytecode. The AST→LLL pass is too big and complex to be condently audited. We
recommend decomposing it into smaller passes with well-dened interfaces. This
decomposition will allow independent auditing of each pass.
During the review, we found a number of bugs, which are described at the end of this
document.
3 Compiler Decomposition
Logically, compilation can be viewed as a sequence of transformations that start with
source code and end with binary code, with different intermediate representations between
each pass. A canonical (non-optimizing) compiler decomposition might be the following:
1. Lexical Analysis: Transforms raw characters into “tokens”. E.g., keywords, variables,
operators, etc.
2. Parsing: Transforms tokens into abstract syntax trees (ASTs). The tree nodes
represent the constructs of the language. E.g., for-loops, if-then-elses, function
declarations, expressions.
3. Semantic Analysis: Determines all the semantic information (a.k.a. “meaning”) of the
represented program. This pass will typically do many of the following:
1. Track the declaration and uses of every identier in the program, and any
necessary information about each identier. E.g., the type of variables, the scope
of variables, etc.
2. Determine the types of all expressions, including any implicit type conversions.
Type checking for expressions includes inferring result types based on operand
types, checking that literals conform to Vyper or EVM limitations, checking
variable/name usage, etc. ( parser/expr.py reports errors in 67 different
locations.)
3. Annotate the ASTs with the type information that is computed. For instance, after
checking that the types of the left and right operands of an ADD node can be
added together, it might annotate the ADD node with the type it will compute.
4. Make explicit what was implicit. Semantic analysis might modify the AST to make
previously implicit operations explicit. For instance, it may add a type conversion
to widen an integer value that is an operand to an ADD where the other operand is
wider. Similarly, it might tag a literal value with how it is going to be used. For
instance, it might tag the literal 7 as a uint256 if it is the operand of an ADD
where the other operand is known to be a uint256 .
4. Constant Folding: Transforms the ASTs based on operations that can be computed at
compile time. E.g., 1+2 can be replaced with 3 .
5. Intermediate Representation (IR) Generation: Translates all of the fully analyzed (and
annotated) ASTs into a chosen IR.
6. Code Generation: Translate the IR into the target code (i.e., EVM bytecode).
The benet of decomposition is that each pass can be analyzed independently. Of course,
this requires that the data structures consumed and generated by each pass be well-
dened. If we ignore lexical analysis (which is done for Vyper by the Python parser) the
decomposition above requires only two well-dened data structures: the AST and the IR.
(The data structure that represents types will, of course, annotate the AST.)
4 Abstract Syntax Trees (AST)
The Vyper compiler’s AST is not an abstract syntax tree for Vyper—it is an AST for Python.
The Vyper compiler translates the Python compiler-generated AST nodes into its own AST
nodes, but those nodes are almost perfect replicas of the original nodes. Thus, the Vyper
compiler is burdened with interpreting Python-like abstract syntax as if it were Vyper
syntax.
A good example of this is the AST nodes for Vyper’s struct and contract constructs.
There aren’t any. Instead, Vyper overloads Python’s class AST node to represent structs
and classes, distinguishing them with a special eld.
There are many examples of where Python’s AST is used to (unnaturally) represent Vyper
syntax:
1. Using Python function call ASTs to represent the following Vyper constructs:
1. events
2. type modiers ( constant , public )
3. type units (e.g. uint256(wei) )
4. maps
2. Using Python class ASTs to represent:
1. structs
2. contracts
3. Using Python’s general for-loop AST to represent Vyper’s restricted for-loop
4. Using Python’s annotated assignment AST to represent
1. implements
2. type declarations
3. custom units declarations
5. Using Python’s decorator AST for Vyper’s function modiers ( @nonreentrant ,
@public , etc.)
5 Separate Type Checking Pass
Vyper’s static type system is suciently complex to merit its own separate pass in the
compiler. This would separate the semantic analysis involved in checking and inferring
type information from the process of generating LLL. The current integration of those
concerns makes understanding either process more dicult and, possibly, more error-
prone. (It may also be hiding opportunities for factoring out common type checking
routines.)
A type-checking pass in the compiler would typically annotate AST nodes with type
information. This is especially useful for expressions and built-in functions, where type
information is inferred from the types of operands and arguments. Once the AST has been
annotated with type information, the LLL generation pass is greatly simplied. This makes
both type checking and LLL generation easier to evaluate/audit.
As an aside, it’s possible that separating type checking into its own compiler pass might
allow the language/compiler’s treatment of typing integer literals to change to be more
context-dependent. That is, perhaps the language/compiler could treat an integer literal to
be whatever type makes the most sense based on how it used rather than based on the
literal itself. For instance, 7 could just as easily be an int128 as a uint256 , and that
choice could be deferred until type checking.
6 Intermediate Representation (LLL)
The Vyper compiler adapts the LLL intermediate representation for its use. The LLL does
not appear to have a specication, which places an extraordinary burden on anybody trying
to understand the IR generation phase.
Similarly, without a precise LLL spec, it is impossible to audit the LLL→bytecode pass of
the compiler.
7 Needed Specications
Before a full security audit of the Vyper compiler is performed, detailed specications
should be written.
7.1 Language Specication
The Vyper language lacks a complete, precise specication, which makes it impossible to
know if the implementation conforms to the desired behavior. A language for implementing
smart contracts must be well-specied so that programmers can be condent that they
know what their smart contracts will actually do.
A specication for Vyper must include a complete description of the syntax, the semantics,
the types, the statements, the operations, etc., of the language. Those descriptions must
be suciently detailed that a conforming compiler could be written from the specication
alone, without need