low

Builtins that access literal lists cannot be compiled

Reward

Total

298.97 USDC

Selected
123.11 USDC
87.93 USDC
87.93 USDC
Selected Submission

Builtins that access literal lists cannot be compiled

Severity

Medium Risk

Relevant GitHub Links

https://github.com/vyperlang/vyper/blob/3ba14124602b673d45b86bae7ff90a01d782acb5/vyper/builtins/functions.py#L460-L463

https://github.com/vyperlang/vyper/blob/3ba14124602b673d45b86bae7ff90a01d782acb5/vyper/builtins/_signatures.py#L82-L103

https://github.com/vyperlang/vyper/blob/3ba14124602b673d45b86bae7ff90a01d782acb5/vyper/semantics/analysis/utils.py#L527

Summary

When types are validated for literal lists passed to builtin functions, we perform the following check:

if not isinstance(expected, (DArrayT, SArrayT)):

However, in this scenario, expected is the type class, not an instance, so it always fails. As a result, the compilation fails.

Vulnerability Details

We will use the builtin len() function to demonstrate this issue.

The len() function accepts a single argument, which can be either a string, byte array or dynamic array:

_inputs = [("b", (StringT.any(), BytesT.any(), DArrayT.any()))]

All builtin functions implement the BuiltinFunction class, which calls the _validate_arg_types() function, which calls self._validate_single() for all arguments.

In the case of the len() function being called with a literal list, the argument passed to _validate_single() is the list node, and the expected type is a tuple of the allowed type classes:

(<class 'vyper.semantics.types.bytestrings.StringT'>, <class 'vyper.semantics.types.bytestrings.BytesT'>, <class 'vyper.semantics.types.subscriptable.DArrayT'>)

This calls the validate_expected_type(), where the given_types returns all the possible types for the literal list.

In the event that the node is a literal list, we go down this code path:

# if it's a literal list, validate: expected contains array, lengths match, each item matches
if isinstance(node, vy_ast.List):
    # special case - for literal arrays we individually validate each item
    for expected in expected_type:
        if not isinstance(expected, (DArrayT, SArrayT)):
            continue
        if _validate_literal_array(node, expected):
            return

As we can see, this checks that isinstance(expected, (DArrayT, SArrayT)). Only when this is the case does it proceed to the _validate_literal_array() function, which allows us to return safely without an error.

Unfortunately, isinstance() tells us if an instance fits a given type. But expected is not an instance — it is the type class itself. As a result, this check will always fail, and the compilation will fail.

Proof of Concept

The following Vyper contracts will fail to compile due to this error:

# @version ^0.3.9

x: uint256

@external
def __init__():
    self.x = len([1, 2, 3])
# @version ^0.3.9

number: public(uint256)
exampleList: constant(DynArray[uint256, 3]) = [1, 2, 3]

@external
def __init__():
    self.number = len(exampleList)

Impact

Contracts that include literal lists as arguments to builtin functions will fail to compile.

Tools Used

Manual Review

Recommendations

In validate_expected_type(), adjust the check to ensure that the expected type matches with DArrayT or SArrayT, rather than requiring it to be an instance of it.