crytic / tealer

Static Analyzer for Teal
GNU Affero General Public License v3.0
62 stars 14 forks source link

Use data flow analysis to find constraints on transaction fields in a contract #96

Open S3v3ru5 opened 2 years ago

S3v3ru5 commented 2 years ago

Dataflow Analysis to find constraints on transaction fields

What are constraints on transaction fields

For each basic block in the CFG

NOTE: only assert instructions are considered to check if execution reverts or not. e.g if sequence of instructions are

...
Global GroupSize
int 3
==
assert
...

Then

Implementation

Source of constraints for a given basic block

  1. Predecessor blocks
  2. Block itself
  3. Successor blocks
block_transaction_context[b] = Union(block_transaction_context[bi] for bi in predecessors[b]) \
                            && block_transaction_context[b] \
                            && Union(block_transaction_context[bi] for bi in successors[b])
# Note: && is intersection

consider

B1     B2
  \    / 
    B3
  /    \
B4     B5

B3 has predecessors B1, B2 and successors B4, B5. Given following transaction contexts,

    # predecessors
    block_transaction_context[B1]["GroupSize] = [1, 3], 
    block_transaction_context[B2]["GroupSize] = [1, 5],
    # block
    block_transaction_context[B3]["GroupSize"] = [1, 3, 5]
    # successors
    block_transaction_context[B4]["GroupSize] = [3], # asserts groupsize is 3 in B4
    block_transaction_context[B5]["GroupSize] = [5], # asserts groupsize is 5 in B5

Then new context will be,

    block_transaction_context = {1, 3, 5} && {1, 3, 5} && {3, 5} = {3, 5}

Branch Based Constraints

Teal contracts generally use conditional branch instructions directly on the constraints(conditional expression). e.g, consider B1 from above is

...
Global GroupSize
int 3
==
bnz B3

This implies

# while considering for B3 
block_transaction_context[B1]["GroupSize"] = [3] # instead of [1, 3] which is the case for other children of B1

To solve this, we can use path based transaction context while considering information from predecessors

# path_transaction_context[bi][b] gives the transaction constraints for path bi -> b
block_transaction_context[b] = Union((block_transaction_context[bi] && path_transaction_context[bi][b]) for bi in predecessors[b]) \
                            && block_transaction_context[b] \
                            && Union(block_transaction_context[bi] for bi in successors[b])
# Note: && is intersection

Algorithm

Phase 1

block_transaction_context = dict()
path_transaction_context = dict()

for B in basic_blocks:
    for F in transaction_fields:
        c = ConstraintsFromAssert(B, F)
        if c == set(): # NullSet(F)
            c = UniversalSet(F)

        block_transaction_context[B][F] = c

        if B[-1] == Ins(BZ) or B[-1] == Ins(BNZ):    # last instruction is conditional branch
            # constraints that must be satisfied to take the jump.
            true_branch_constraints = ComputeTrueBranchConstraints(B, F)
            # constraints if the default branch is taken
            false_branch_constraints = ComputeFalseBranchConstraints(B, D) # default branch

            if true_branch_constraints = set(): # NullSet(F)
                true_branch_constraints = UniversalSet(F)

            if false_branch_constraints = set(): # NullSet(F)
                false_branch_constraints = UniversalSet(F)

            # successors[0] is the default branch and successors[1] is the target branch

            path_transaction_context[successors[B][0]][B][F] = false_branch_constraints
            path_transaction_context[successors[B][1]][B][F] = true_branch_constraints

Phase 2

worklist = [b for b in basic_blocks]

while worklist:
    B = worklist[0]
    worklist = worklist[1:]
    new_transaction_context = merge_information(B)
    if new_transaction_context != block_transaction_context[B]:
        worklist.extend(predecessors(B))
        worklist.extend(successors(B))
        worklist = list(set(worklist))

Transaction Fields to consider for constraints

Global Fields:

  1. GroupSize: Finite -> [1, 16]

Transaction Fields:

  1. GroupIndex: Finite -> [0, 15]
  2. Type: Finite -> [pay{}, keyreg{}, acfg{}, axfer{}, afrx{}, appl{Creation, UpdateApplication, DeleteApplication, NoOp, ...}]
  3. RekeyTo: Addr -> Store concrete constraints only
  4. CloseRemainderTo: Addr
  5. AssetCloseTo: Addr
  6. Sender: Addr
  7. Receiver: Addr ...

Note

Uses

Blockers

ToDo

Related/Solves issues #75, #82, #83, #87, #90, #95

S3v3ru5 commented 2 years ago

The above dataflow equations do not work correctly when there's a backedge(loops). Because:

Ex:

Screenshot 2022-10-13 at 8 14 36 PM

information from instructions txn OnCompletion == int CloseOut in block 4 would not reach block 9 and 10. Similarly, information from instructions txn OnCompletion != int DeleteApplication in block 9 (inside the loop) would not reach block 4 and other blocks outside the loop.

An easy solution for this would be to:

However, we are doing path-sensitive analysis. so, if the information is part of the back edge itself and is not part of the source or target block of the edge, we would lose that information. But, this should be fine for the use cases. Because, in practice, transaction constraints are not part of the loop conditions.

Another solution is to change the equations to follow GEN and KILL type analysis. And update the implementation accordingly.