Guiding the Search
This section introduces the hints
section as a tool [V] developers can use in order to help guide
the search for counterexamples against the input specification.
Intro to Hints: A Small Example
Hints are used to guide the fuzzer's search. If the fuzzed transaction has a require statement or satisfying the spec requires a specific contract state, random fuzzing will not help without spending excessive time. In those cases, the users can provide hints to improve the quality of fuzzing.
Some ways hints can guide fuzzing include:
- Providing values to the transaction arguments
- Providing values to the sender/value fields of a transaction
- Providing constraints over transaction arguments or contract variables.
Below, we have an example function:
function foo(uint256 x, uint256 y) public onlyOwner {
require(x > 100000);
require(y < x);
...
}
In this function, there are three constraints that are difficult to randomly satisfy.
x
must be greater than 100000.y
must be less thanx
.onlyOwner
limits valid callers to only the contract owner.
To pass these checks, the users can write [V] hints in the form of finished(<target>, <hint-program>)
where target function's arguments or sender/value fields will be modified based on the hint program. The hint program is a sequence of assignments or for-all expressions containing other hint programs to modify the arguments.
An example for the function foo
is given below.
vars: Contract c
hints: finished(c.foo(x, y),
x := elem_in_range(100001, MAX_UINT256);
y := elem_in_range(0, x);
sender := c.owner
)
spec: ...
The hint above has three assignments to make it run successfully.
x
is assigned a random value between 100001 and MAX_UINT256 to satisfyrequire(x > 100000)
.y
is assigned a random value between 0 andx
to satisfyrequire(y < x)
.sender
is assigned to contract's owner to satisfy theonlyOwner
requirement.
Another way to write this hint would be to use solve
expressions. This method invokes an SMT solver, so it is going to be slower to run but for complex cases, it might be useful. In the solve
expressions, users can express constraints in [V]. The constraints are solved by an SMT solver and the expression returns the values to be used in assignment.
For example, the hint above can also be expressed as:
vars: Contract c
hints: finished(c.foo(x, y),
(x, y) := solve{uint256 a, uint256 b}(a > 100000 && b < a);
sender := c.owner
)
spec: ...
In the hint above, solve
block calls SMT solver to find a satisfying solution to the provided constraint in the solve
block, and returns a
and b
values satisfying the provided constraint as a tuple for assignment to the left hand side.
Now You're Thinking with Portals: Enhancing Hints with Complex Expressions
As seen in the previous section, hints can be constructed as series of assignments with or without using solve
blocks to pass requirements in transactions. For some complex require statements (on arrays or structs), users will need to use solve
blocks or forall
expressions to be able to express their constraints.
Below, we have an example function comparing two lists:
function countVotes(address[] voters, uint256[] votes) public {
# Require a non-empty voters and votes list
require(voters.length == votes.length && voters.length > 0);
# Only accepting votes for 4 candidates (1, 2, 3, 4)
for(uint i=0; i<votes.length; i++){
require(votes[i] < 5 && votes[i] > 0);
}
...
}
For these two require statements, we can write a [V] hint like below:
vars: Contract c
hints: finished(c.count_votes(voters, votes),
(voters, votes) := solve{address[] _voters, uint256[] _votes}(
len(_voters) = len(_votes) && len(_voters) > 0);
forall{i : range(0, len(votes))}(votes[i] := elem_in_range(1, 5))
)
spec: ...
These two expressions modify voters
and votes
to be able to pass the requirements:
- The first expression solves a constraint on
_voters
and_votes
to make them equal-length and non-empty, then assigns those lists tovoters
andvotes
respectively. - The second assignment modifies each element of the
votes
to be within the values 1-4.
In this example, calling solve
lets us randomly generate 2 arrays with equal length which we could not do in [V]. If we did not use solve
to write the hint, we could have fixed the length of both arrays with an expression like votes := [elem_in_range(1, 5), elem_in_range(1, 5)]
. That would have been faster but that would also limit the possible values to fuzz for that function to a small subset.
General Hint Syntax
The hint grammar can be described as below:
HintSequence : Hint
| Hint ; HintSequence
Hint: finished(Target, HintProgram)
HintProgram : LHSExpr := Expr
| forall(I : I)(HintProgram)
| HintProgram ; HintProgram
Target : I.I(I, ...)
| I.I
| I.*
| *
LHSExpr : I
| LHSExpr.I
| LHSExpr[I]
| (LHSExpr, ...)
Expr : SolveExpr
| ConstraintExpr
HintSequence
represents a single hint or a sequence of hints, separated by a semicolon ;
. Hint
represents a single hint description with Target
describing the function to match on and HintProgram
describing the sequence of assignments to perform to modify the functions.
Target
represents the target of the statement, similar to the definition in [V] statements.
HintProgram
represents a hint program, consisting of an assignment expression, a for all block containing a hint program, or a sequence of hint programs. LHSExpr
represents any identifier, field access, array access, or tuple containing any of the previous expressions where each element has to match an argument of Target
, sender
, or value
. I
represents any identifier. Expr
represents a solve expression (explained in the next chapter) or a constraint expression and it needs to return the type of LHSExpr
when evaluated.
General Solve Syntax and Behavior
To increase the expressibility of hints, solve
expressions let users call an SMT solver and get a solution for their arguments. The solve
expression can be described as the following:
SolveExpr: solve{SMTVarDecl}(ConstraintExpr)
SMTVarDecl: VarType VarName
| SMTVarDecl, SMTVarDecl
VarType: address
| string
| bool
| uint # Behaves same as uint256
| uint<num> # uint8, uint16, ..., uint256 are allowed
| int256 # Behaves same as int256
| int<num> # int8, int16, ..., int256 are allowed
| bytes<num> # Only bounded byte variables are allowed
| VarType[] # Array types
VarName: <str>
SolveExpr
describes the expression which returns the concrete values to variables in SMTVarDecl
based on the constraints in ConstraintExpr
. SMTVarDecl
lets users create undefined variables which only appear in the ConstraintExpr
, similar to the free variables defined in vars
section but only limited to primitive types and arrays described in VarType
. ConstraintExpr
is a [V] expression which will be translated into SMTLIB to be solved.
There are two main differences between ConstraintExpr
in solve blocks and [V] constraints:
- For binary expressions, only primitive types (
address
,bytes
,bool
,uint
,int
,string
) are allowed.x = 1
is a valid expression,x = [1, 2, 3]
is an invalid expression. For modifying array, struct, and enum variables, the users can express assignments in hints such asx := [1, 2, 3]
,x[0] := ...
, orx.field := ...
. - For binary expressions on bytes, the lengths of left hand side and right hand side expressions have to match exactly. For example,
x = b"00cafe00"
is valid only whenx
is of typebytes4
, otherwise solver translation returns a type error.
As a warning, there may be some limitations in terms of the constraints OrCa can solve as SMT solvers (like Z3) may have difficulty providing solutions in a short amount of time where the constraints contain nested arrays or complex mathematical expressions like quadratics. If the solver times out or constraints have a type mismatch, OrCa will terminate to warn the user not to provide such constraints.
See Also
For more details on [V] hints and hint functions, please refer to Hint Description and Useful Hint Functions.